개요
React와 같은 Single Page 앱을 Nginx를 통해 배포할 때 해줘야 하는 설정들이 있습니다.
예를 들어, React에서 BrowserRouter를 사용할 때에는 Nginx의 try_files와 같은 설정을 해주어야 합니다.
하지만 여러 개의 React 앱을 하나의 Nginx로 배포할 때에는 추가적인 설정이 필요할 수가 있습니다.
오늘은 여러개의 React 프로젝트를 하나의 Nginx를 통해 배포하는 방법에 대해 알아보도록 하겠습니다.
React 앱을 설정 없이 Nginx로 배포했을 때 발생하는 문제
빌드된 React 앱을 단순하게 Nginx의 웹루트로 배포해도 처음에는 잘 동작됩니다.
react-router를 쓰지 않는 React 앱일 경우에는 크게 문제가 없이 페이지전환도 잘 됩니다.
하지만 react-router-dom 라이브러리를 사용하여 react-router를 사용할 때 처음에는 잘 동작하는 듯 보이나 페이지 이동 후, Refresh를 하게 되면 404 not found가 발생하게 됩니다.
그 이유는 Nginx가 클라이언트 측에 라우팅을 인식하지 못해서 발생하게 되는데요, React-Router를 통해 클라이언트 측의 라우팅을 처리하는 경우, 서버는 해당 경로에 대한 파일을 찾을 수 없기 때문에 404 not found 오류가 발생합니다.
1개의 React 앱을 Nginx로 배포하기
server {
...
location / {
try_files $uri $uri/ /index.html;
}
...
}
Nginx에서 발생하는 404 not found 에러는 위와 같이 nginx.conf 파일을 수정하여 해결할 수 있습니다.
Nginx의 try_files 지시어는 정적 파일 처리 및 요청에 관련된 Redirection을 처리하는 지시어입니다.
try_files $uri $uri/ /index.html 이라고 명시를 해주면 Nginx가 해당 경로의 정적 파일을 먼저 찾아보고 없으면 React의 index.html를 반환하도록 구성할 수 있습니다. 이후에는 React Router가 해당 경로를 처리하고 해당 페이지를 표시할 수 있게 됩니다.
(예를 들어, /about 페이지에 대한 요청이 있을 경우, nginx는 먼저 /about 파일을 찾고 없으면 /about/ 으로 redirect한뒤 그래도 없으면 index.html을 반환하게 됩니다.)
여러 개(2개 이상)의 React 앱을 Nginx로 배포하기
Nginx를 이용하여 각 서브 디렉토리별로 여러 개의 nginx앱을 배포하기 위해서는 조금 더 추가적인 설정을 해주어야 합니다.
아래는 설정해줘야할 목록들입니다.
- nginx의 nginx.conf 파일 try_files 설정
- React Router의 basename 설정
- 정적 리소스(파일)을 가져오는 경로 올바르게 맞춰주기
각각에 대해 설정하는 방법에 대해 예제와 함께 알아보도록 하겠습니다.
저희는 https://test.com/ 도메인에 apps 폴더를 만들고 demo1, demo2 각각의 React 앱을 배포한다고 가정해 보도록 하겠습니다.
- 첫 번째 React 앱의 주소 : https://test.com/apps/demo1
- 두 번째 React 앱의 주소 : https://test.com/apps/demo2
1) nginx의 nginx.conf 파일 try_files 설정
각각의 서브 디렉토리로 React 앱을 배포하기 위해서는 nginx의 요청이 들어왔을 때, try_files를 통해 서브 디렉토리에 있는 React 앱의 각각의 index.html로 이동시켜줘야 합니다.
예를 들어, React 앱 2개(demo-1, demo-2)를 apps 폴더 안에 demo-1, demo-2 서브 디렉터리로 배포한다고 가정해 보도록 하겠습니다.
이때,
/apps/demo-1으로 요청이 들어왔을 때에는 /apps/demo-1/index.html으로 요청을 redirect 시켜주고,
/apps/demo-2으로 요청이 들어왔을 때에는 /apps/demo-2/index.html으로 요청을 redirect 시켜주는 설정이 필요합니다.
http {
server {
listen 80;
server_name your_domain.com;
# demo1 설정
location /apps/demo-1 {
try_files $uri $uri/ /react-apps/demo-1/index.html;
}
# demo2 설정
location /apps/demo-2 {
try_files $uri $uri/ /react-apps/demo-2/index.html;
}
location / {
}
}
}
위와 같이 설정을 하면 apps 서브 디렉토리 하위로 배포된 각각에 앱들(demo-1, demo-2)에 대해 올바르게 redirection이 처리됩니다.
즉, /apps/demo-1/ 하위로 요청 하게 되면 첫 번째 react 앱이 동작하고 /apps/demo-2로 하위로 요청을 하게 되면 두 번째 react앱이 동작하게 됩니다. 새로고침을 할 때도 물론 404 에러는 발생하지 않습니다.
배포할 때마다 nginx.conf 파일을 바꾸는 것이 번거로우면 아래와 같이 처리할 수도 있습니다.
# Any route that doesn't have a file extension
location ~* ^/apps/([a-z0-9_-]+) {
try_files $uri $uri/ /apps/$1/index.html;
}
위와 같이 정규표현식을 이용하여 설정하면 여러 번 nginx.conf 파일을 수정하지 않아도 apps의 하위폴더로 새로 만든 react 프로젝트들을 배포할 수 있습니다.
간략하게 설명드리면, apps 안에 새로 생성되는 폴더를 통해 React 앱을 배포하고 폴더의 이름을 $1 변수로 받아서 해당 폴더 하위 요청에 대해 해당 폴더 하위의 index.html로 redirect해주는 설정입니다.
위처럼 설정하면, 직접 폴더 이름을 명시하지 않아도 apps 하위의 서브 디렉터리로 앱을 배포하면 자동으로 해당 폴더에 대한 try_files 설정이 완료됩니다.
2) React Router의 basename 설정
React Router의 basename이란, 라우팅 경로에서 기본적으로 사용되는 경로를 설정하는 옵션입니다.
보통 애플리케이션의 기본 경로가 서브 디렉토리에 위치할 경우 basename 설정을 해주어야 합니다.
보통, React Router의 BrowserRouter의 basename을 nginx의 서브 디렉토리와 같게 설정하여 React Router가 경로를 올바르게 찾을 수 있도록 설정합니다.
예를 들어, 아래는 /apps/demo-1 의 BrowserRouter 설정입니다.
import { BrowserRouter } from 'react-router-dom';
const IntroPage = () => {
return <h1>Welcome to the Intro Page!</h1>
}
// 브라우저 라우터의 기본 경로 설정
ReactDOM.render(
<BrowserRouter basename="/apps/demo-1">
<Route path="/intro" component={InTroPage}/>
</BrowserRouter>,
document.getElementById('root')
);
위와 같이 basename을 제대로 설정해 주면 Nginx의 서브 디렉터리로 배포한 React App을 nginx가 제대로 라우팅 할 수 있게 됩니다.
하지만, basename을 제대로 설정해 주지 않으면 /intro 페이지를 찾을 때, /apps/demo-1/intro에서 찾는 것이 아니라 /intro 에서 찾기 때문에 404 에러가 발생할 수 있습니다.
3) 정적 리소스(파일)를 가져오는 경로 올바르게 맞춰주기
왜 정적 리소스 경로를 맞춰 주어야 할까?
마지막으로 해야 할 작업은, 요청하는 정적 리소스에 대한 경로를 잘 설정해 주는 것입니다.
정적 리소스란 CSS나 이미지 파일, 빌드한 bundle.js 파일들을 의미합니다.
이에 대한 설정을 잘해주지 않으면 이미지나 리소스에 대한 404 에러가 발생할 수 있습니다.
예를 들어 /apps/demo-1 으로 배포하게 되면 해당 React 앱의 정적 파일들은 보통 https://test.com/apps/demo-1/하위에 존재하게 됩니다.
만약 아래와 같이 첫 번째 React 앱(demo-1)의 bundle 파일을 가져오기 위한 코드가 /apps/demo-1/index.html 에 있다고 생각해 보겠습니다.
그러면 bundle 리소스의 위치는 https://test.com/apps/demo-1/static/js/bundle.js 에 존재하게 됩니다.
<script src="/static/js/bundle.js"></script>
만약 bundle 파일을 가져오기 위해 위의 코드처럼 선언하게 되면 되면, 웹 서버의 루트를 기준으로 파일을 찾게 됩니다.
즉, 파일을 찾는 경로는 https://test.com/static/js/bundle.js 가 되기 때문에 제대로 bundle 파일을 가져오지 못하고 404 에러가 발생하게 됩니다.
<script src="./static/js/bundle.js"></script>
/* 또는 */
<script src="static/js/bundle.js"></script>
또 만약 위의 코드처럼 설정이 되어있으면 어떻게 될까요?
만약 index.html 파일이 https://test.com/apps/demo-1/index.html 경로에 존재한다면 그 위치로부터 리소스를 찾게 되기 때문에, 올바른 bundle.js 파일을 가져옵니다. (현재 위치가 /apps/demo-1 이므로 /apps/demo-1/static/bundle.js 로 찾음)
하지만 index.html의 위치가 달라진다면 리소스의 위치가(상대 경로) 달라지게 되므로 항상 올바르게 bundle을 가져올 수는 없습니다.
만약, /react/apps/demo-1/path/index.html에 있다고 가정해 보면 404 에러가 발생합니다. (현재 위치가 demo-1/path이므로 /apps/demo-1/path/static/js/bundle.js)
<script src="/apps/demo-1/static/js/bundle.js"></script>
따라서 index.html에서 bundle.js 파일을 찾기 위해서는 위와 같이 설정되어야 합니다.
또 다른 예를 들어, React 내부적으로 이미지를 가져오는 컴포넌트가 있다고 가정해 보겠습니다.
return (
<div>
<img src="images/image.jpg" alt="My Image" /> // 상대경로로 이미지 요청
</div>
)
위에서 처럼 src를 선언하여 가져오면 어떻게 될까요? 마찬가지로 서버에서는 https://test.com/images/image.jpg 를 통해 정적 이미지를 가져오게 됩니다.
return (
<div>
<img src="https://test.com/apps/demo-1/images/image.jpg" alt="My Image" /> // 상대경로로 이미지 요청
</div>
)
실제로는 https://test.com/apps/demo-1/images/image.jpg 에 이미지가 있기 때문에 위와 같이 가져와야 합니다.
보통 위와 같은 설정을 하기 위해서는 CRA와 같은 프로젝트 시작도구 등에서 설정하는 방법과 index.html을 통해 <base href />를 이용하여 페이지 내의 모든 상대주소들의 기본 URL을 설정하는 방법이 있습니다.
그러면 아래부터는 CRA 프로젝트 및 직접 Webpack을 설정한 프로젝트 별로 정적 이미지 경로를 설정하는 방법에 대해 각각 알아보겠습니다.
3-1) CRA(create react app)을 이용하여 프로젝트를 구성한 경우 정적 파일 리소스 경로 설정
CRA를 통해 프로젝트를 구성한 경우에는 package.json의 homepage 속성을 설정하여 정적 파일에 대한 Root URL을 설정할 수 있습니다.
{
"name": "demo-1",
"version": "0.0.1",
"homepage": "https://mywebsite.com/apps/demo-1", // 정적 파일 루트 URL 설정
"scripts": {
"start": "react-scripts start",
...
...
...
}
예를 들어, 위와 같이 pacakge.json을 설정한 뒤 CRA 빌드를 통해 빌드 파일을 추출할 수 있습니다.
"homepage": "/apps/demo-1" 로 경로만 지정할 수도 있습니다.
경로만 지정하는 경우, 애플리케이션은 호스팅 된 도메인의 기본 경로에 상대적으로 위치합니다. 이렇게 함으로써, 애플리케이션을 서브 디렉토리에 배포할 수 있고, 다른 경로에 배포되는 다른 애플리케이션과 충돌하지 않습니다.
반면에 도메인까지 지정하는 경우, 애플리케이션은 명시적으로 지정된 도메인과 경로에서만 작동합니다. 이는 단일 도메인 내에서 여러 애플리케이션을 호스팅 하고자 할 때 유용합니다.
그러면 https://mywebsite.com/apps/demo-1 에 대한 Root URL이 설정된 빌드 파일이 추출되고 해당 빌드파일을 Nginx 웹서버의 서브 디렉터리로 배포하면 됩니다. app2도 똑같이 진행하시면 됩니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My React App</title>
</head>
<body>
<div id="root"></div>
<script src="%PUBLIC_URL%/static/js/main.js"></script>
</body>
</html>
이후 index.html에서 js파일이나 이나 css를 가져올 때 %PUBLIC_URL% 을 이용하면 됩니다. %PUBLIC_URL%은 공용 폴더를 가리키는 경로로 치환됩니다.
즉 %PUBLIC_URL%/static/js/main.js는 빌드 이후 /apps/demo-1/static/js/main.js 로 바뀌게 됩니다.
import React from 'react';
const MyComponent = () => {
const imageUrl = `${process.env.PUBLIC_URL}/images/my-image.jpg`;
return (
<div>
<img src={imageUrl} alt="My Image" />
</div>
);
};
export default MyComponent;
컴포넌트에서 이미지를 가져올 때는 기본 url 경로를 지정하기 위해 CRA에서 제공하는 환경변수인 process.env.PUBLIC_URL 를 사용할 수 있습니다.
실제로 ${process.env.PUBLIC_URL}/images/my-image.jpg 경로는 빌드 시, /apps/demo-1/images/my-image.jpg 로 치환됩니다.
process.env.PUBLIC_URL 을 꼭 쓰지 않으셔도 됩니다.
예를 들어 /apps/demo-1/images/my-image.jpg 를 경로로 설정하거나, /image/my-image.jpg, image/my-image.jpg 이런 식으로 설정하셔도 /apps/demo-1/images/my-image.jpg 경로로 올바르게 잘 가져옵니다.
<참고>
추가적으로 보통 리엑트에서는 정적 이미지 경로를 직접 명시하여 이미지를 가져오는 방법보다 소스코드에 포함시켜서 import 하는 방식을 좀 더 많이 사용하는 것 같습니다. 즉 보통은 아래와 같이 이미지를 가져옵니다.
import React from 'react';
import myImage from './images/my-image.jpg';
const MyComponent = () => {
return (
<div>
<img src={myImage} alt="My Image" />
</div>
);
};
export default MyComponent;
webpack의 file-loader 등을 이용하면 사용된 이미지 파일들을 지정한 폴더로 복사하고, 번들링 된 파일이 배포될 때 이미지 파일의 경로를 자동으로 생성하여 사용된 곳에 대체합니다.
3-2) Webpack을 이용하여 직접 프로젝트를 구성한 경우 정적 파일 리소스 경로 설정
위에서는 CRA를 통해 정적 리소스 파일들을 가져오는 경로를 설정하는 방법에 대해 알아보았습니다.
이번에는 CRA를 쓰지 않고 일반적인 Webpack을 통해 빌드를 진행할 때, 즉 webpack.config.js를 직접 구성해서 빌드할 때, 정적 리소스에 대한 기본 URL 경로를 설정하는 방법에 대해 알아보도록 하겠습니다.
만약 서브 디렉토리로 배포했을때 경로를 지정하지 않고 루트 경로로 정적 리소스를 가져오게 되면 404에러가 발생할 수 있습니다.
이때 서브 디렉토리로 배포될 경로에 대해 <base href="기본 URL 주소" /> 를 지정하여 정적 리소스 경로를 쉽게 가져올 수 있도록 설정할 수 있습니다.
예를 들어 /apps/demo-1 로 리엑트 앱이 배포된다고 가정해 보도록 하겠습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8"/>
<base href="/apps/demo-1" />
<script src="/static/js/main.js"></script>
</head>
...
...
위와 같이, <base /> 태그를 이용하여 페이지 내의 모든 URL 경로에 대한 기본 url(절대 경로)을 설정할 수 있는데요,
이후에 번들 파일을 가져오기 위해 /static/js/main.js를 <script/> 태그로 가져오는 요청을 보내면 /apps/demo-1/static/js/main.js 로 변환되어 웹서버로 리소스 요청을 보내게 됩니다.
import React from 'react';
const MyComponent = () => {
return (
<div>
<img src={'/images/my-image.jpg'} alt="My Image" />
</div>
);
};
export default MyComponent;
컴포넌트에서 이미지를 가져올 때에도 base 태그가 적용됩니다. 따라서 위와 같이 이미지를 가져올 때 실제 경로는 /apps/demo-1/images/my-image.jpg 가 되어 해당 경로에서 정적 이미지를 가져올 수 있게 됩니다.
마무리
오늘은 React 앱 여러 개를 서브 디렉토리로 배포하는 방법에 대해 알아보았습니다.
생각보다 nginx 서버 쪽과 React 코드 쪽에서 수정해 줘야 될 내용이 많아서 이를 정리하다 보니 포스팅이 길어진 것 같습니다.
추가적으로 궁금하신 내용이 있으면 답글 달아주시면 빠른 시간 내로 답변드리겠습니다.
감사합니다.
'서버 인프라, 백엔드 > 웹서버 (Nginx)' 카테고리의 다른 글
Nginx : X-Forwarded-For(XFF) 헤더를 통해 IP 로그 남기기 (0) | 2024.06.13 |
---|---|
Nginx : autoindex 를 통한 파일 목록 출력하기 (0) | 2024.03.11 |
Nginx : alias vs root 지시어의 차이점 알아보기 (0) | 2023.09.24 |