(이 글을 Infra와 Next.js 두 개의 카테고리 중 어디에 넣어야 할지 고민했는데 글의 중심 주제는 Next.js의 output: export 설정과 그로 인한 것들이라 Next.js 자체에 좀 더 집중되어 있다고 봐서 Next.js 카테고리에 넣었습니다.)
들어가기 전에
다음 과정은 "output: export" 방식으로 정적 빌드한 Next.js App Router 프로젝트를 배포하는 과정입니다. 배포하는 EC2 환경에는 Node.js가 없고, 이 과정에서 Docker는 사용하지 않습니다. 그러므로 프로젝트를 정적 빌드한 산출물을 그대로 EC2 인스턴스 위에 올려서 배포하며, Node 프로세스 매니저인 PM2도 사용하지 않습니다.
도대체 왜 이런 짓을 하는가? 이번 배포를 시도하는 이유
정말 EC2 배포환경에 노드 런타임이 없어도 정적 배포(Static Exports)를 한다면 Next.js 프로젝트 배포가 가능한지 궁금했습니다.
정적 배포(Static Exports)
https://nextjs.org/docs/app/guides/static-exports
Guides: Static Exports | Next.js
Next.js enables starting as a static site or Single-Page Application (SPA), then later optionally upgrading to use features that require a server.
nextjs.org
Next.js 정적 배포 설정(빌드까지)
저는 EC2 위에 노드가 없는 상황을 가정했으므로 프로젝트 빌드 또한 EC2위에서 할 수 없습니다. 빌드는 따로 하고 그 빌드 결과물 폴더를 EC2로 옮겨서 배포해야 합니다. 그러기 위해서 빌드를 먼저 완료하겠습니다.
next.config.ts
const nextConfig: NextConfig = {
output: "export",
}
정적 배포에서는 아래와 같은 Next.js의 서버 관련 고유 기능들을 사용할 수 없습니다. 프로젝트에 다음과 같은 기능들이 활성화돼있다면 삭제하거나 주석처리 해주어야 합니다. 그렇지 않다면 런타임 또는 빌드타임에 에러가 발생합니다.
(위 공식문서를 보면 export const dynamic = 'error'를 루트 레이아웃에 추가하는 방식으로도 동적 렌더링이 발생하지 않게 할 수 있는 것으로 보입니다. 하지만 저는 하나씩 제거하겠습니다. )
동적 렌더링 강제 옵션 삭제
Next.js의 서버 컴포넌트는 기본적으로 동적 요소(쿠키, 헤더 등 그리고 아래에서 언급할 다이나믹 라우팅도)가 없다면 정적으로 렌더링(빌드타임 렌더링)됩니다.
"force-dynamic"은 이렇게 기본적으로 정적 렌더링 페이지(빌드타임 렌더링)를 동적 렌더링(런타임 렌더링)되게 하기 위하여 컴포넌트 상단에 추가하는 옵션입니다.
export const dynamic = "force-dynamic";
프로젝트 코드에 위와 같이 동적 렌더링을 강제하는 "force-dynamic"이 있다면 주석처리합니다. 해당 컴포넌트는 이제 동적으로 렌더링 되지 않습니다. 빌드타임에 정적으로 렌더링됩니다.
이미지 최적화 끄기
아래와 같이 next의 Image 태그를 사용하는 부분이 있다면
import Image from "next/image"
export default function Board() {
return (
<Image
src="/assets/logos/others/Github_logo.svg"
alt="깃허브 로고"
width={48}
height={48}
/>
);
}
다음과 같이 config파일에 unoptimized: true 옵션을 추가해서 이미지 최적화 기능이 동작하지 않게 해 줍니다.
next.config.ts
const nextConfig: NextConfig = {
output: "export",
images: {
unoptimized: true,
}
}
GenerateStaticParams 적용하기
동적 라우팅(dynamic routing) 페이지에 generateStaticParams를 추가
정적 배포 시 프로젝트 내에 동적 라우팅 페이지가 있다면 generateStaticParams 함수를 사용해야 합니다.
동적 라우팅 페이지는 다음과 같이 정의한 페이지입니다.
/app/project/[id]/page.tsx
동적 라우팅 페이지는 기본적으로 동적으로 렌더링됩니다.
하지만 정적 배포 옵션에서는 더 이상 동적 렌더링을 사용할 수 없습니다. (동적 렌더링은 SSR과 항상 동일한 것은 아닙니다. 저도 이걸 뒤늦게 깨달았는데 App Router의 동적 렌더링은 RSC(리액트 서버 컴포넌트)의 동적 렌더링을 의미합니다. SSR은 페이지를 새로 고침하거나 직접 주소를 입력할 때 발생합니다.)
이 페이지는 "동적 렌더링(dynamic rendering)"되는 "동적 라우팅(동적 세그먼트를 포함해 온디맨드 방식으로 페이지가 렌더링 됨)"페이지입니다. 현재 이 페이지에는 generateStaticParams가 존재하지 않습니다.
page.tsx
import getOneSupabaseProjectData from "@/services/getOneSupabaseProjectData";
import OneProject from "@/component/OneProject/OneProject";
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const resolvedParams = await params;
const id = parseInt(resolvedParams.id);
const { data: projectData, error } = await getOneSupabaseProjectData(id);
if (error || !projectData) {
console.error("에러 발생", error);
return (
<div>프로젝트 데이터를 불러오는데 실패했거나 데이터가 없습니다.</div>
);
}
return (
<div>
<OneProject {...projectData} />
</div>
);
}
그럼에도 위의 강제적 동적 렌더링 전환(force-dynamic)과 이미지 최적화 기능을 사용하는 즉시 에러가 발생한 것과 다르게 개발 환경에서는 별다른 에러가 발생하지 않습니다.
하지만 빌드해 보면 어떨까요?
npm run build
바로 에러가 발생합니다.
Build error occurred [Error: Page "/project/[id]" is missing "generateStaticParams()" so it cannot be used with "output: export" config.]
동적 라우팅 페이지는 generateStaticParams 없이 output:export와 사용할 수 없다고 합니다. generateStaticParams를 이 페이지에 추가해서 동적 렌더링 → 정적 렌더링 페이지로 바꾸어주어야 합니다.
그럼 이 페이지는 정적으로 렌더링되는 동적 라우팅 페이지가 됩니다. 아닙니다. 이제 더 이상 동적(dynamic) 라우팅이라는 표현은 적절하지 않습니다. 정확히 말하자면 빌드 타임에 정적 경로(static route)를 생성하게 되는 거니까요. 이건 동적 세그먼트([id])를 포함한 정적 경로를 만든 것이지 더 이상 동적으로 라우팅되는 것이 아닙니다.
라우팅과 렌더링을 잘 구분하세요.
결국 정적 배포란 정적 내보내기(Static exports)인 것입니다. 정적 배포 상태에서는 정적 렌더링과 정적 라우팅만 하겠다는 것이고, 더 이상 동적 렌더링과 동적 라우팅은 하지 않습니다. 이 렌더링 부분을 이해하는 것이 Next.js App Router의 핵심인 것 같습니다.
Functions: generateStaticParams | Next.js
API reference for the generateStaticParams function.
nextjs.org
generateStaticParams를 사용하는 것은 몇 번부터 몇 번까지의 정적 페이지를 만들어둘지를 미리 알려주는 것입니다. 정해져 있어야 빌드 타임에 렌더링을 완료할 수 있기 때문입니다.
저 같은 경우 전체 프로젝트의 개수를 알기 위해 모든 프로젝트 데이터를 반환하는 함수를 가져와 프로젝트들의 id를 generateStaticParams에서 반환했습니다.
page.tsx
import getAllSupabaseProjectData from "@/services/getAllSupabaseProjectData";
import getOneSupabaseProjectData from "@/services/getOneSupabaseProjectData";
import OneProjectBackground from "@/component/OneProject/OneProjectBackground";
export async function generateStaticParams() {
const { data: projectListData } = await getAllSupabaseProjectData();
return (
projectListData?.map((post) => ({
id: String(post.project_id),
})) || []
);
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const resolvedParams = await params;
const id = parseInt(resolvedParams.id);
const { data: projectData, error } = await getOneSupabaseProjectData(id);
if (error || !projectData) {
console.error("에러 발생", error);
return (
<div>프로젝트 데이터를 불러오는데 실패했거나 데이터가 없습니다.</div>
);
}
return (
<div>
<OneProjectBackground data={projectData} />
</div>
);
}
여기까지 완료하면 이제 빌드에 성공할 것입니다.
npm run build
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
이제 빌드 결과에 f(Dynamic)이 없습니다.
그리고 generateStaticParams를 사용한 페이지는 SSG라고 표시되는군요.
Static Rendering !== SSG(Static Site Generation)
generateStaticParams는 정적 페이지(html)를 미리 생성해 둔다는 점에서 SSG(Static Site Generation, 정적 사이트 생성)가 맞습니다. 하지만 정적 렌더링이 항상 SSG를 의미하는 것은 아닙니다. SSG는 페이지(html)에 대응되는 개념이고 정적 렌더링은 컴포넌트(tsx)에 대응되는 개념이기 때문입니다. generateStaticParams는 page 컴포넌트에만 사용할 수 있습니다. 그리고 이 page.tsx(혹은 page.jsx)는 Next.js App Router에서 특별한 의미가 있죠.
그 말은 page가 아닌 다른 컴포넌트(ex) BoardDetail.tsx)를 정적 렌더링해도 이 자체를 SSG(Static Site Generation)이라고 부를 수는 없다는 것입니다. 정적 렌더링이라는 부분에서는 동일하지만 Site(page)가 아니기 때문입니다. 사실 지금까지는 정적 렌더링이 곧 SSG이기도 했습니다. 왜냐하면 한 페이지는 전부 동적 렌더링되거나 아니면 전부 정적 렌더링되기 때문입니다.
한 페이지에 A,B 컴포넌트가 들어있을때 A 컴포넌트는 별다른게 없다면 기본적으로 정적 렌더링 됩니다. 그런데 B 컴포넌트에 동적 렌더링을 강제하는 코드(헤더 등)가 있다면 자동적으로 A도 포함하여 페이지 전체가 동적 렌더링되었습니다. 그런데 현재 실험적 기능인 ppr(partial pre-rendering)으로 인해 한 페이지에 동적 렌더링되는 컴포넌트와 정적 렌더링 되는 컴포넌트를 같이 사용해도 분리된 렌더링 방식을 적용할 수 있게 되기 때문에 앞으로는 아닐 가능성이 높기 때문입니다. 물론 아직 실험적 기능이고 안정화되지 않았지만, 지금까지의 실험적 기능들의 행보를 보면 이 기능도 거의 분명해보입니다. 지금도 Next.js App Router의 렌더링 방식은 충분히 복잡한데 이 기능 또한 공식적으로 추가되면 어떻게 될지 벌써부터 두렵기도 하고 기대되기도 합니다.
(너무 길어서 이 부분은 다른 글로 분리 예정)
out이라는 정적 배포를 위한 빌드 결과물이 담겨있는 폴더가 생겼습니다. (output:export 추가 후에도 .next폴더(일반 빌드 결과물)도 같이 생깁니다. 일반 빌드의 경우 .next 폴더만 생깁니다.)
여기서부터는 EC2 터미널에서 진행
EC2 배포는 제가 지난번에 진행했던 과정 그대로 하겠습니다. 다만 이번 배포에서는 Node.js를 사용하지 않을 것이니 nvm(node) 설치 과정은 패스하겠습니다.(npm도 마찬가지, pm2도 설치하지 않을 것입니다.)
[AWS EC2] Next.js 프로젝트 배포하기 (ubuntu + nginx + pm2)
들어가기 전에 다음 과정은 매우 간단한 Next.js App Router 프로젝트를 배포하는 과정입니다. AWS 가입 과정, 탄력적 IP연결, HTTPS설정, 도메인 등록, Docker 사용, 환경 변수 관리 등의 내용은 이 글에 포
unhandled.tistory.com
위 과정에서 EC2 인스턴스 생성 - 키 페어 파일로 인스턴스 접속 - 우분투 시스템 업데이트&업그레이드 - nginx 설치까지 완료합니다.
빌드한 결과물을 옮길 폴더부터 생성합니다.
sudo mkdir -p /var/www/html/without-node-app
리버스 프록시 설정은 이렇게 해줍니다. 정적 배포를 하는 것이니 일반 배포를 할 때와 다르게 작성해주어야 합니다. (저 폴더명이 생성한 폴더와 일치해야 합니다. 저는 without-node-app으로 통일했습니다.)
server {
listen 80;
server_name _;
root /var/www/html/without-node-app;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.html;
}
}
리버스 프록시를 수정한다면 문법 확인은 필수입니다.
sudo nginx -t
아래와 같은 내용이 콘솔에 출력되면 다음 단계로 넘어갑니다.
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
(위 메세지가 아닌 에러가 발생하면 리버스 프록시를 설정한 나노 파일에 문법이 틀린 부분이 있는 것입니다)
이제 우분투 사용자(EC2 인스턴스에 접속한 저)가 저 폴더에 접근할 권한도 주셔야 합니다. 아래 명령어를 EC2 터미널에 치지 않으면 프로젝트 결과물 복사 자체가 불가능합니다.
sudo chown ubuntu:ubuntu /var/www/html/without-node-app
앞으로 진행하다가 무언가 에러가 발생하면 nginx의 에러 로그와 액세스 로그를 확인해 보세요. 로그만으로 곧바로 정답을 찾기는 어렵지만 단서를 주기도 합니다.
nginx 에러 로그 조회
sudo tail -f /var/log/nginx/error.log
nginx 액세스(접근) 로그 조회
sudo tail -f /var/log/nginx/access.log
EC2 인스턴스에 빌드 산출물 옮기기(out 폴더 내용물)
(중요) 아래 명령어는 EC2 인스턴스에 접속해 있는 터미널 말고 프로젝트 루트 폴더에서 또 다른 터미널을 열어서 쳐주어야 합니다.
그리고 저는 아래 명령어를 쳐주었습니다. (여기서도 옮길 폴더명이 위에서 언급한 것과 동일해야 합니다!)
scp -i /C:/Users/kakap/Desktop/without_node_aws/without_node.pem -r out/* ubuntu@ec2-12-345-678-90.ap-northeast-2.compute.amazonaws.com:/var/www/html/without-node-app/
현재 루트의 out 폴더의 모든 내용물을 저 경로에 있는 키페어 파일(.pem 파일)을 통하여 해당 EC2 인스턴스 내의 var/www/html/without-node-app이라는 경로(아까 만들어 놓은 폴더)에 복사하라는 것입니다.
참고로 저 명령어에서
out/* → out폴더 안에 있는 모든 파일을 가져와
입니다.
만약 저 부분에 ./out을 쓰게 되면 → out폴더를 가져와
가 됩니다.
저는 생성해 놓은 폴더에 out 폴더 하위 내용물을 전부 옮기는 전자의 방식을 택했습니다. 만약 out폴더 자체를 가져오는 후자의 방식을 택했다면 아까 작성한 리버스 프록시의 root를 수정해주어야 합니다.
server {
listen 80;
server_name _;
// out/*으로 가져올시
root /var/www/html/without-node-app;
// ./out으로 가져올시
root /var/www/html/without-node-app/out;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.html;
}
}
/without-node-app/ → /without-node-app/out/
여기서 뭔가 꼬이면 403 에러를 바로 마주하게 됩니다. 이걸 왜 알려주냐면... 묻지 마세요..
성공하면 복사되는 폴더와 파일들 목록이 아래에 주르륵 나옵니다. (만약 여기서 에러가 발생한다면 오타가 있거나 위 우분투 사용자에게 권한을 주지 않았을 확률이 높습니다. 경로, 폴더명, 키파일명, Ip4 주소 등을 정확하게 확인하고 입력해 주세요.)
이제 다시 EC2 인스턴스 터미널로 돌아와 nginx를 재시작하고 배포 결과를 확인해 보겠습니다.
sudo systemctl restart nginx
정적 배포 결과
오.. 배포가 되긴 합니다. 근데 CSS 스타일링이 싹 날아갔네요? (그러고 보니 이 화면 뭔가 낯익은..)
CSS로 구현한 인터렉션 기능도 안되고 CSS 스타일링이 제대로 적용되지 않은 것처럼 보입니다. 인라인 스타일링을 적용한 요소들은 제대로 CSS가 적용된 것을 보니 별도 CSS파일 관련하여 문제가 있군요.
기술 스택 리스트의 로고 이미지는 Supabase에서 호스팅 중인 이미지를 가져온 것입니다. 잘 뜹니다.
위 파란색 박스 안의 펜로즈 삼각형 로고는 Supabase 스토리지에 호스팅 중인 것을 가져온 것인데 얘도 잘 뜹니다. 다만 이 로고 좌우에 위치하는 정적 에셋 이미지(프로젝트 폴더 내 이미지 파일에서 가져오는)들은 제대로 뜨질 않네요.
이외에 다른 내용들을 보니 프로젝트 관련 데이터(프로젝트 개요 등)는 Supabase에서 잘 가져오고 있는 것 같습니다. 이건 대체 무슨 상황일까요??
브라우저 콘솔창에는 정체불명의 SyntaxError가 발생했습니다. '<'을 찾을 수 없다니?
실제로 네트워크 창 열어보니까 JS번들은 상태 번호 200으로 잘 도착하는 것처럼 보입니다. 그런데 까보면 자바스크립트 코드가 아니라 죄다 html이 들어있습니다;; 당황스럽군요.
로컬 out폴더에 들어있는 동명의 JS번들은 내용물이 멀쩡한데 말입니다.
[진실]
근데 이건 제가 실수로 JS번들을 HTML로 바꿔버린 게 아니라 에러가 발생했기 때문에 리버스 프록시에서 index.html을 반환해서 이렇게 보인 것입니다. 즉, index.html 자체가 첫 랜딩페이지니까요. 리버스 프록시 설정이 에러를 만나면 index.html을 반환하게 되어있고 그래서 네트워크 탭의 JS번들 Response에 HTML이 들어있는 것처럼 보인 것입니다. 위의 Syntax 에러는 JS코드를 기대했는데 <!DOCTYPE html>로 시작하는 HTML파일이 들어오니까 HTML의 첫 번째 문자인 "<" ← 이건 뭐냐? 라고 얘기한 거였습니다.
리눅스 퍼미션
이건.. 알고 보니 리눅스 퍼미션(권한)과 관련한 문제였습니다. 저는 이런 것을 처음 접해봐서 신기합니다. 위의 파일 복사시 주었던 권한과는 또 다른 권한 이슈인 것 같습니다.
이렇게 조회하면 해당 폴더 안의 파일과 폴더들 목록이 나옵니다.
ls -l /var/www/html/without-node-app
맨 앞의 d는 디렉토리(폴더)를 의미하고, 맨 앞이 -일 경우 파일을 의미한다고 합니다.
그 뒤에 따라오는 부분은 저렇게 3개의 영역으로 나눌 수 있습니다.
첫 번째는 User 두 번째는 Group, 마지막 3개는 Others입니다.
여기서 EC2로 접속한 저(우분투)는 User도 Group도 아닌 Others로 분류되나 봅니다. 외부의 빌드 결과물을 EC2로 옮긴 것이라 그런 것 같습니다. (아마 EC2 위에서 빌드를 했다면 퍼미션 문제는 발생하지 않았을지도..? 이건 단지 제 추측입니다.) 왠지 이 퍼미션 부분은 더 파고들면 끝이 없을 것 같아서 여기까지만 이해하고 진행하겠습니다.
위 캡처를 보시면
index.html 파일: -rw-rw-r--
index.html 파일은 3번째 권한(우분투 유저)이 읽는 건 가능(r)합니다. 그래서 첫 페이지 자체가 보일 수는 있던 거고요.
_next, assets, project, test-1 디렉토리: drwx------
얘네들은 아예 우분투 사용자가 권한이 없네요? 읽기, 쓰기, 실행 중 아무런 권한이 없습니다. 아마 저 폴더에 정적 이미지 파일 CSS 파일 다 들어있을 텐데 말입니다.
# /var/www/html/without-node-app 디렉토리와 그 하위 모든 것에 대해
# 1. 디렉토리에 다른 사용자들(Others)이 읽고(r) 실행(x)할 수 있도록 755 권한 부여
sudo find /var/www/html/without-node-app -type d -exec chmod 755 {} \;
# 2. 파일에 다른 사용자들(Others)이 읽을 수 있도록(r) 644 권한 부여
sudo find /var/www/html/without-node-app -type f -exec chmod 644 {} \;
drwx------
→ drwxr-xr-x
이제 디렉토리(폴더)의 권한도 수정되었습니다.
퍼미션 부여 후
오오 잘 나옵니다!!
결국 아까 CSS 스타일링이 제대로 적용되지 않고 정적 이미지 에셋이 뜨지 않았던 것은 CSS 파일과 정적 이미지들이 들어있는 폴더에 접근할 수 없어서 생긴 퍼미션(권한) 문제였습니다.
정적 이미지 에셋도 잘 뜹니다.
뿌ㅡ듯
다이나믹 세그먼트를 포함한 SSG 페이지 접근 에러
근데 또 이상한 부분이 있습니다.
/project/1, /project/2 이런 generateStaticParams적용한 정적 경로 페이지(SSG)에 직접 접속하면 그냥 메인페이지만 보입니다. 이번에는 또 뭐가 또 문제야!
아하 이건... 리버스 프록시 설정이 Next.js의 정적 배포가 아니라 리액트 배포 기준으로 작성되어 있어서 발생한 이슈였습니다.
아까 작성한 리버스 프록시는 이렇게 되어있습니다.
server {
listen 80;
server_name _;
root /var/www/html/without-node-app;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.html;
}
}
여기서 저 location 부분을 수정해야 합니다.
//원래 리버스 프록시 설정
location / {
try_files $uri $uri/ /index.html;
}
//이렇게 수정
location / {
try_files $uri $uri.html $uri/ /index.html;
}
수정한 리버스 프록시
server {
listen 80;
server_name _;
root /var/www/html/without-node-app;
index index.html index.htm;
location / {
try_files $uri $uri.html $uri/ /index.html;
}
}
이 location 부분의 try_files는 파일을 찾으라는 것입니다. $uri 이 부분은 정적 에셋 등의 파일을 찾을때 찾고, 페이지 요청의 경우 index.html을 fallback합니다. 리액트에서는 모든 일이 index.html 안에서 일어나기 때문에 페이지 요청 시 항상 index.html을 반환하면 되니까요.
이건.. try_files(다음 파일들을 찾아라)에 $uri.html이 추가된 거잖아요?
그러니까 이게 알고 보니.. 왜 오류가 났냐면 만약 이게 순수 리액트 앱이었으면 리액트는 SPA니까 html파일이 index.html 단 하나밖에 없습니다. 근데 얘는 아니죠. 이 프로젝트는 정적배포한 next.js 앱입니다. 이는 next.js의 정적 빌드 과정에서 정적 렌더링이 발생하는 각각의 라우트 페이지에 전부 1:1 대응되는 html 파일을 생성합니다.
제가 여기다 generateStaticParams를 추가했으므로 project/1, project/2 이런 경로에 해당하는 페이지들도 생성되었고, 이들은 project/1.html, project/2.html 여기에 존재합니다. 근데 찾는 대상에 $url.html이 없으니까 못 찾은 겁니다. 랜딩 페이지 뜨는 것도 못 찾았으면 index.html 보여줘(next에서는 app/page.tsx에 해당하는 페이지)라고 해놓았기에 index.html을 fallback한 것이고요.
잠깐만.. index.html을 찾으라고 했더니 못 찾았다..(index.html을 찾으라고 했다기보단 project.html 이런 다른 html도 찾는 과정이 있어야하는데 그것이 누락된 것입니다.) 그 이유가 project/1.html, project.html 이런 다른 페이지를 담당하는 html파일들이 있어서? 잠깐.. 그럼 얘는 리액트랑 달리 Single Page Application이 아닌 거잖아?( ← 이 부분에 대해서는 저의 오해가 섞여있는데 내용이 길어서 다른 글로 정리합니다. 짧게 언급만 한다면 index.html만이 존재하는 것이 아닐 뿐 Next.js는 SPA로서 동작합니다.)
[Next.js] App Router는 SPA(Single Page Application)인가?
MPA(Multi Page Application)SPA 등장 이전의 방식(JSP, vanilla JS(+jquery)) 은 MPA(Multi Page Application)이었습니다. 이 방식은 페이지 이동 시 서버에서 새로운 HTML을 직접 그려서 클라이언트로 보내줍니다.(이것
unhandled.tistory.com
이건 다이나믹 라우트 때문이라기보단 next.js가 결국 구조적으로는 SPA(index.html만을 사용)가 아니라 MPA(여러 페이지에 대응되는 반환하는 html파일이 존재)과 유사한 구조이기에 생긴 이슈입니다. "구조적"으로는 MPA와 유사한데 "기능적"으로는 SPA과 같은 것입니다. 위 링크 글을 읽어주세요.
그래서 저 location에 $uri.html을 찾아라(여기서 $uri.html는 제 경우에는 1.html 입니다.)는 명령이 필요한 거고요.
이제 성공했습니다!
마무리
이 테스트는 일주일 전쯤에 해본 건데 이제 글을 적게 되네요. 일주일 간 Next.js의 렌더링 방식이나 특성에 대해 정말 많은 생각을 해보았습니다. 사실 본문에 적은 RSC와 SSR의 차이라던가 SPA와 MPA 구분에 대한 언급은 이 테스트 이후 많은 생각들을 거쳐서 알게 된 사실들입니다.
그리고 이 포스팅에서는 수월하게 진행되는 듯(?)하지만 여기에 다 적지 않은 우여곡절들이 있었습니다. 비표준적인 방법을 시도한 것이라 그런지 원활하게 진행되지는 않더라고요. 위의 퍼미션 에러도 scp 명령어 같은 원시적인 방법으로 프로젝트 빌드 결과를 서빙해서 발생한 에러인 것 같습니다. (Docker를 사용했다면.. 도커는 고립된 컨테이너니까 아마 발생하지 않지 않았을까요?)
하지만 제가 확인하고 싶은 것을 직접 확인했고 무엇보다 이번 경험의 의의는 생각해 볼 만한 거리가 많아졌고 그 덕에 좀 더 Next.js에 대해 잘 이해할 수 있게 되었다는 것 같습니다. 지난 수개월간 계속 잘못 이해하고 있던 개념들이 많았다는 것도 이번에 깨닫게 되었는데 어쨌든 Next.js를 공부할 때 처음으로 마주하게 되는 큰 산을 방금 넘은 것 같아서 그게 매우 뿌듯합니다.
*글의 밀도가 너무 과도하게 높은 것 같아 중간중간의 특정 단락은 다른 포스팅으로 분리해야할 것 같습니다. 글의 구조도개선하고 정리하고 가독성도... 하지만 일단.. 지금은 업로드하고 쉬어야할 것 같아요...
'FE > Next.js' 카테고리의 다른 글
[Next.js] App Router의 클라이언트 컴포넌트의 자바스크립트는 서버에서 실행될 수도 있다 (0) | 2025.05.23 |
---|---|
[Next.js] App Router는 SPA(Single Page Application)인가? (0) | 2025.05.20 |
[Next.js] AWS EC2로 Next.js를 배포할 때 Node.js를 설치해야 하나요? (0) | 2025.05.16 |
[Next.js] 왜 Next.js는 "풀스택" 프레임워크라고 불리는가? (0) | 2025.05.03 |
[Next.js] 포트폴리오 성능 점수 100점 도전하기 (feat. 라이트하우스, 폰트 최적화) (0) | 2025.05.01 |