본문 바로가기
카테고리 없음

아파치 리버스 프록시 설정 + 정규표현식

by 잘먹는 개발자 에단 2025. 6. 27.

 

전에 있던 배포 문제 때문에 리버스 프록시 설정을 고려하게 되는데, 이 경우 중요한 제약사항이 있다.

 

- 서버 전체의 수정을 할 수 없다.

- 많은 서비스가 돌아가기 때문에 서버를 껐다 킬 수 없다.

- 서버 전체의 설정을 건드리지 않고, 배포 단위 ( 폴더 ) 로만 설정하고 싶다.

 

그러면 어떻게 해야하는가?

 

 

서버의 메인설정파일인 httpd.conf를 직접 수정하는 대신에, .htaccess 파일을 사용하여 디렉토리 ( 폴더 ) 레벨에서 설정을 적용할 수 있다.

 

.htaccess 파일의 내용은 서버를 재시작하지 않아도 즉시 반영된다.

다만 ProxyPass 지시어는 보안상의 이유로 .htaccess 파일 내에서 사용하는 것이 기본적으로 허용되지 않는다. 

따라서 다른 대안을 사용해야하는데, 아파치의 강력한 기능인 mod_rewrite 모듈을 사용하면 .htaccess 파일 내에서 리버스 프록시와 동일한 효과를 낼 수 있다. 

 

 

사용하려면 다음과 같은 사전 조건이 필요하다.

1. 서버 관리자가 아파치 서버에 mod_rewrite와 mod_proxy, mod_proxy_http 모듈을 미리 활성화해 두었어야 한다. 

(대부분 기본적으로 활성화 되어있다. ) 

2. 서버 설정에서 .htaccess 파일을 통한 설정변경 AllowOverride이 허용되어있어야 한다.

 

 

설정방법

1. .htaccess 파일 위치

- 서버 A에 배포하신 리액트 빌드 결과물이 있는 폴더의 최상위에 .htaccess 파일을 생성하거나 수정한다. 보통 index.html 파일과 같은 위치이다.

 

2. .htaccess 파일 내용 작성

- 아래 내용을 .htaccess 파일에 추가하거나 기존 내용과 병합합니다. 

- React Router 같은 클라이언트 사이드 라우팅을 사용하고 계실 것을 감안하여, 해당 규칙까지 포함된 전체 예시입니다.

 

# 아파치 재작성 엔진 활성화
RewriteEngine On


# --- API 리버스 프록시 설정 ---
# '/api/'로 시작하는 모든 요청을 서버B로 전달한다.
# [P] 플래그가 프록시 역할을 하도록 지시한다.
# [L] 플래그는 이 규칙이 마지막 규칙임을 의미한다. ( 이 규칙이 적용되면 아래 규칙들은 무시한다 )
RewriteRule ^api/(.*)$ http://<서버B_내부IP>:<API_포트>/api/$1 [P,L]


# -- React Router를 위한 설정
# 요청된 파일이나 디렉토리가 실제로 존재하지 않는 경우에만 아래 규칙을 적용한다.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# 모든 요청을 index.html로 보내서 React Router가 처리하도록 한다.
RewriteRule . /index.html [L]

 

- ^api/(.*)$ : http://서버A/api/ 로 시작하는 모든 주소를 의미한다.

- http://<서버B_내부IP>:<API_포트>/api/$1 : /api/ 뒷부분의 경로 ( (.*) )를 그대로 가져와 ($1) 서버 B의 주소에 붙여준다. 예를 들어서 /api/users는 http://서버B/api/users로 요청된다. 

- 참고로 RewriteRule은 들어온 요청URL에 따라 순차적으로 검사된다. 

위와 같은 요청을 만약에 JS로 나타낸다면 다음과 같다. 

function handleRequest(url) {
  if (url.startsWith('/api/')) {
    // API 프록시 처리
    // 여기서 끝냄 (L 플래그의 역할)
    return; 
  }

  // 위의 if 문에 걸리지 않은 요청만 이리로 내려옴
  if (!isFile(url) && !isDirectory(url)) {
    // React 라우터 처리 (index.html 로 보내기)
    // 여기서 끝냄 (L 플래그의 역할)
    return;
  }
}

 

 

 

간단한 정규 표현식을 알아보자.

 

1. ^ 캐럿

- 문자열의 시작을 의미하는 앵커이다.

- ^api라고 쓰면, URL 경로가 반드시 api라는 글자로 시작해야만 일치(매칭)한다.

- 매칭 O : /api/users, /api/products

- 매칭 X : /myapi/users, /v1/api/

 

 

2. $ 달러 기호

- 문자열의 끝을 의미하는 앵커이다.

- world$라고 쓰면 문자열이 반드시 world라는 글자로 끝나야만 매칭된다.

- 매칭 O : helloworld, world

- 매칭 X : worldcup

 

 

3. (.*) (괄호, 점, 별표)

- 이것은 세가지 요소의 조합이다.

- 1. . (점,dot)

    ㄴ 어떤 종류의 문자든지 한글자 를 의미하는 와일드카드이다. 숫자 알파벳 특수문자 등 거의 모든 것을 의미하며, 단 줄바꿈 문자를 제외이다.

 

- 2. * (별표, Asterisk)

    ㄴ 바로 앞에 있는 문자가 0번 이상 반복됨을 의미한다.

    ㄴ a* : a가 없거나 ( ), 하나 있거나 ( a ), 여러개 ( aa, aaa ..) 있는 경우 모두 매칭된다.

 

- 3. .* (점 + 별표 ):

    ㄴ 위 두개를 합치면 어떤 문자든지 (.) 0번 이상 반복 (*) 이라는 뜻이 된다.

    ㄴ 그래서 결론적으로 모든 문자열을 의미한다. 문자열이 아예 없어도 되고, 아무리 길어도 된다.

 

- () (괄호, Parentheses)

    ㄴ 그룹으로 묶고 그 내용을 캡쳐한다는 매우 중요한 역할이다. 캡쳐는 기억 또는 저장한다고 생각하면 쉽다.

    ㄴ (.*) 는 .*가 찾아낸 모든 문자열 부분을 하나의 그룹으로 묶어서 기억해둔다. 이렇게 기억된 내용은 나중에 $1, $2 같은 변수로 재사용할 수 있다. ()가 첫번째 그룹이므로 $1로 참조할 수 있다.

 

그러면 이제 ^api/(.*)$ 의 의미를 맞춰보자.

- 이것은 다음과 같은 규칙을 가진 URL 경로를 찾는다.

- 경로가 api/로 시작하고 (^api/), 그 뒤에 오는 모든 문자열 ( (.*) )을 첫번째 그룹으로 캡쳐하며, 그 문자열로 경로가 끝나야 한다.   ( $ ). 

 

 

 

그러면 예시를 한번 보자.

사용자가 http://서버A/api/users/123 주소로 접속했다고 가정해보자.

 

1. URL 경로 /api/users/123 을 정규 표현식 ^api/(.*)$과 비교한다.

2. ^api/ : 경로가 api/로 시작하므로 매칭된다.

3. (.*) : api/ 뒤에있는 users/123 전체가 .* (모든 문자열)에 해당한다.  ()가 이 users/123 부분을 캡쳐하여 $1이라는 변수에 저장한다. 

4. $ : users/123으로 경로가 끝나므로 매칭된다.