Nginx를 통한 SPA 앱 배포 시 주의 및 설정법 (404 핸들링, 정적 애셋 처리)

시소·2024년 2월 26일
1
post-thumbnail

Nginx를 통해 SPA 앱을 배포하는 건 간단해 보일 수 있지만, 클라이언 트 측 라우팅(React Router 등)을 사용하는 경우 404 에러가 올바르게 핸들링 되어야 한다.
지금에서야 알고 보니 매우 사소한 내용이었지만 모르고 있었을 때엔 '왜 안되지' 하고 답답해하였던 기억을 되새기며 기록을 남겨 본다.


프로젝트 구성 및 기존 설정

‣ Dependencies

  • vite v5.0.8
  • react v18.2.0
  • react-router-dom v6.22.0
  • ...

‣ Directory Structure

# 일부 생략 
./src
├── App.tsx
├── main.tsx
└── pages/
    ├── Chat.tsx
    ├── Login.tsx
    └── Register.tsx

‣ Routing

  • /: 유저 인증 정보가 있으면 Chat 컴포넌트를 보여주고, 없다면 Login 페이지로 리디렉션
  • /login: Login 페이지
  • /register: Register 페이지

‣ Dockerfile

# Stage 1: Build
FROM node:lts-alpine as build

WORKDIR /usr/src/app

COPY package.json /usr/src/app/package.json
COPY package-lock.json /usr/src/app/package-lock.json
RUN npm ci

COPY . /usr/src/app

RUN npm run build # vite build 명령이 실행된다.

# Stage 2: Production Image
FROM nginx:alpine

COPY --from=build /usr/src/app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

문제 확인

로컬에서 npm run start 를 통해 실행시킨 개발 환경에서는 문제 없었지만, 빌드를 통해 번들링 한 내용을 Nginx로 서빙하면서 다음 문제를 발견하였다.
처음에는 라우팅에 문제가 없다가, 브라우저 새로 고침을 하는 순간 404 Not Found를 마주하게 된다.

원인은 vite build 를 통해 생성된 /dist 디렉터리를 보면 짐작할 수 있다. 별다른 vite.config.ts 설정 없이 기본 구성으로 vite에서 빌드를 수행했을 때 "index.html"과 "assets/index-XXX.js" 이라는 2개의 정적 파일이 생성되었는데, 이 /dist 디렉터리를 Nginx에 그대로 제공하였으니 디렉터리 내부에는 "/login/index.html" 이나 "/register/index.html" 이라는 파일이 없으니 못 찾는게 당연하다.

과거 경험 상 vite 가 아닌 Next.js 에서 빌드를 수행했을 땐, 라우팅 별로 HTML 파일을 생성하는 빌드 옵션이 있었는데(Static Exports), vite 에는 그런 비슷한 옵션이 있나 찾아봤더니 Multi page app 이라는 기능이 있기는는 하여서 한 번 살펴 보았다. 그러나 일반적으로 단일 entry point를 가지고 있는 리액트 앱의 특성과 잘 맞지 않는 구조라고 생각되어 적절치 않은 해결법이라 생각하였다.

따라서 Nginx config를 변경하여 해결하기로 하였다.


변경한 부분

프로젝트 루트에 아래 nginx.conf 파일을 신규 작성하였고, 기존의 Dockerfile을 수정하였다.

추가한 nginx.conf 파일 설정 내용에 대해 살펴보자면, SPA의 경우 클라이언트에서 라우팅을 처리하므로 웹서버 측에서는 항상 index.html 파일을 반환해야 한다. 이를 위해 설정에서 URL 경로에 따라 항상 index.html 파일을 제공하도록 설정한 내용이다.

처음에는 /etc/nginx/conf.d 경로에 nginx.conf 파일을 추가하여도 브라우저에 들어가 새로고침 해보면 계속 파일을 찾을 수 없다고 하길래 설정이 잘못된 줄 알았다.([error] 23#23: *1 open() "/usr/share/nginx/html/login" failed (2: No such file or directory))

/etc/nginx/conf.d 위치에 내가 작성한 nginx.conf 이외에도 기본적으로 default.conf 라는 디폴트 설정 파일이 있었기 때문에 설정 파일이 겹침으로 인해 문제가 반복된 것으로 보인다.
따라서 Dockerfile에서 사용하지 않는 설정인 default.conf 파일을 제거함으로써 내가 원하는 처리를 수행할 수 있었다.

‣ nginx.conf

server {
    listen 80;
    server_name client;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files               $uri $uri/ /index.html;
    }

    error_page 404              /index.html;

    error_log                   /var/log/nginx/error.log;
    access_log                  /var/log/nginx/access.log;
}

‣ Dockerfile

# Stage 1: Build
FROM node:lts-alpine as build

WORKDIR /usr/src/app

COPY package.json /usr/src/app/package.json
COPY package-lock.json /usr/src/app/package-lock.json
RUN npm ci

COPY . /usr/src/app

RUN npm run build

# Stage 2: Production Image
FROM nginx:alpine

COPY --from=build /usr/src/app/dist /usr/share/nginx/html
COPY --from=build /usr/src/app/nginx.conf /etc/nginx/conf.d # 추가
RUN rm /etc/nginx/conf.d/default.conf # 추가

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

+) 정적 파일 다루기

브라우저 캐싱을 통한 반복된 요청 최소화

vite에서 개발할 때 /public 디렉터리에 정적 애셋(이미지, 아이콘, 폰트 등)을 보유하고 있는 경우가 있다. 이런 경우 빌드를 하게 되면 해당 폴더의 내용이 번들에 자동으로 포함되기 때문에 /public 폴더에 대한 별도 설정이 크게 필요하지 않을 수 있다.

다만 프로덕션 환경이라면 정적 파일을 제공할 때 Nginx 설정을 통해 최적화하는 것을 고려할 수 있다.
고민해 볼 사안으로는 브라우저 캐싱, 압축, 보안 등에 대한 설정 등이 존재한다. 그리하여 예로 캐싱을 위한 옵션에 대해 한 번 알아보았다.

다음은 파비콘을 추가하였을 때의 예시와 처리 방법이다. Nginx 설정을 하려면 별도의 경로가 필요하므로, 기존에 .js 파일이 위치하던 /assets 경로와 구분되도록 빌드 시에는 /public 위치에 있는 애셋들을 모두 /dist/static 에 모아두도록 하였다.

‣ Directory Structure

.
├── Dockerfile
├── index.html
├── nginx.conf
├── package-lock.json
├── package.json
├── public/
│   └── static/
│       └── favicon.ico
├── src/
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

‣ index.html (아래 부분 추가)

<link rel="icon" type="image/ico" href="/static/favicon.ico" />

이후 빌드를 해보면 /dist 디렉터리에 새로 추가한 favicon.ico 애셋이 생성된다.

./dist
├── assets
│   └── index-jDoZkisC.js
├── index.html
└── static
    └── favicon.ico

다음으로는 Nginx 에서 캐싱을 위한 설정 옵션을 추가했다. 해당 내용은 1달동안 정적 파일에 대해 브라우저 캐싱을 설정한다는 내용이다.

‣ nginx.conf (아래 부분 추가)

location /static/ {
    expires 30d;
    add_header Cache-Control "public, max-age=2592000"; 
}

설정 후 Nginx를 다시 실행하여 브라우저에서 확인해 보면 파비콘이 적용된 모습과, Network 탭에서 확인했을 때 캐시된 결과(HTTP Status 304)로부터 제공되는 걸 볼 수 있다.
이외에도 Nginx 설정에는 다른 유용한 옵션이 많으니 프로젝트 환경과 요구 사항에 맞도록 적절히 적용할 수 있어야 할 것 같다.


참고 자료

profile
배우고 익힌 것을 나만의 언어로 정리하는 공간 ..🛝

0개의 댓글