전에 있던 배포 문제 때문에 리버스 프록시 설정을 고려하게 되는데, 이 경우 중요한 제약사항이 있다.
- 서버 전체의 수정을 할 수 없다.
- 많은 서비스가 돌아가기 때문에 서버를 껐다 킬 수 없다.
- 서버 전체의 설정을 건드리지 않고, 배포 단위 ( 폴더 ) 로만 설정하고 싶다.
그러면 어떻게 해야하는가?
서버의 메인설정파일인 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으로 경로가 끝나므로 매칭된다.