From 1e801687a5d765b284164622acfb4b908635b578 Mon Sep 17 00:00:00 2001 From: Minhyeok Kim Date: Tue, 26 May 2026 19:14:51 +0900 Subject: [PATCH] =?UTF-8?q?docs(4=EC=A3=BC=EC=B0=A8):=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20=EC=82=AC=EC=9D=B4=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "week-04/\353\257\274\355\230\201.md" | 416 ++++++++++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 "week-04/\353\257\274\355\230\201.md" diff --git "a/week-04/\353\257\274\355\230\201.md" "b/week-04/\353\257\274\355\230\201.md" new file mode 100644 index 0000000..96a4efe --- /dev/null +++ "b/week-04/\353\257\274\355\230\201.md" @@ -0,0 +1,416 @@ +# SPA와 서버 사이드 렌더링 + +## 싱글 페이지 애플리케이션(SPA) + +싱글 페이지 애플리케이션(Single Page Application; SPA)이란 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 **브라우저의 JavaScript에 의존하는 방식**이다. + +최초에 필요한 HTML, JavaScript, CSS 리소스를 내려받은 이후에는 서버에서 새로운 HTML을 매번 받아오지 않고, 하나의 페이지 안에서 화면 전환과 사용자 interaction을 처리한다. + +즉, 페이지 이동이 발생해도 실제로는 `` 전체를 새로 내려받는 것이 아니라, 브라우저에서 JavaScript가 필요한 화면만 바꿔주는 방식에 가깝다. + +예를 들어 Gmail 같은 서비스는 최초 진입 시 필요한 리소스를 불러오고, 이후 메일 목록 이동이나 메일 상세 확인 같은 동작을 브라우저 안에서 빠르게 처리한다. 이때 필요한 데이터나 일부 리소스는 Lazy Loading 방식으로 나중에 불러올 수 있다. + +## SPA의 장점 + +SPA는 최초 로딩 이후에는 서버를 거쳐 새로운 HTML을 받아올 일이 적다. + +그래서 한 번 로딩된 이후에는 페이지 전환이 빠르고, 사용자에게 앱과 비슷한 UI/UX를 제공할 수 있다. + +## SPA의 단점 + +SPA는 최초에 로딩해야 할 JavaScript 리소스가 커질 수 있다. + +또한 렌더링을 브라우저의 JavaScript가 담당하기 때문에, 사용자 기기의 성능에 영향을 받을 수 있다. + +--- + +# 서버 사이드 렌더링 + +서버 사이드 렌더링(Server Side Rendering; SSR)이란 최초에 사용자에게 보여줄 페이지를 **서버에서 렌더링한 뒤**, 완성된 HTML을 브라우저에 전달하는 방식이다. + +SPA는 사용자에게 제공되는 JavaScript 번들에서 렌더링을 담당한다. + +반면 서버 사이드 렌더링은 렌더링에 필요한 작업을 서버에서 수행한 뒤 HTML을 내려준다. + +따라서 사용자 기기의 성능에 비교적 덜 의존하고, 안정적으로 초기 화면을 제공할 수 있다. + +--- + +# 서버 사이드 렌더링의 장점 + +## 1. 최초 페이지 진입이 비교적 빠르다 + +서버 사이드 렌더링은 서버에서 렌더링된 HTML을 브라우저에 전달한다. + +브라우저가 JavaScript를 다운로드하고 실행한 뒤 화면을 만드는 SPA 방식보다, 초기 화면을 더 빠르게 보여줄 수 있는 경우가 있다. + +특히 화면 렌더링이 API 요청에 의존적이거나, 렌더링해야 할 HTML의 크기가 크다면 서버에서 미리 HTML을 만들어 내려주는 방식이 더 유리할 수 있다. + +## 2. 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다 + +검색 엔진 로봇이나 SNS 공유 봇은 페이지의 HTML을 읽어 검색 정보나 공유 정보를 가져간다. + +이때 HTML 내부의 오픈 그래프(Open Graph), 메타(Meta) 태그 정보를 기반으로 제목, 설명, 썸네일 등의 정보를 확인한다. + +SPA는 대부분의 화면 생성이 JavaScript에 의존한다. + +반면 서버 사이드 렌더링은 최초 렌더링 작업이 서버에서 일어나기 때문에, 검색 엔진이나 SNS에 제공할 정보를 HTML 응답에 미리 포함할 수 있다. + +따라서 검색 엔진 최적화(SEO)나 SNS 공유 정보 제공에 대응하기 쉽다. + +## 3. 누적 레이아웃 이동이 적다 + +누적 레이아웃 이동(Cumulative Layout Shift)이란 사용자에게 페이지를 보여준 이후에 뒤늦게 HTML 정보가 추가되거나 삭제되어 화면이 덜컥거리는 것처럼 보이는 현상이다. + +SPA에서는 콘텐츠가 API 요청 결과에 따라 나중에 채워지는 경우가 많다. + +이때 API 응답 속도가 제각각이면 화면 요소의 위치가 뒤늦게 바뀌면서 누적 레이아웃 이동 문제가 발생할 수 있다. + +반면 서버 사이드 렌더링은 필요한 요청이 완료된 이후 완성된 HTML을 제공할 수 있기 때문에, 상대적으로 이러한 문제를 줄일 수 있다. + +## 4. 보안에 좀 더 안전하다 + +서버 사이드 렌더링은 인증이나 민감한 데이터 처리를 서버에서 수행하고, 브라우저에는 결과만 전달할 수 있다. + +따라서 클라이언트에 노출되면 안 되는 로직이나 데이터를 서버에 둘 수 있다는 장점이 있다. + +--- + +# 서버 사이드 렌더링의 단점 + +## 1. 항상 서버 환경을 고려해야 한다 + +서버 사이드 렌더링에서는 코드가 브라우저가 아니라 서버에서도 실행될 수 있다. + +따라서 `window`, `document`, `sessionStorage`, `localStorage`처럼 브라우저에만 존재하는 전역 객체를 사용할 때 주의해야 한다. + +이런 코드는 클라이언트에서만 실행되도록 분리해야 한다. + +다만 클라이언트에서만 실행되는 코드가 많아질수록 서버 사이드 렌더링의 장점을 일부 잃을 수 있다. + +## 2. 적절한 서버가 구축되어 있어야 한다 + +SPA는 정적 HTML, JavaScript, CSS 리소스를 제공할 수 있는 환경만 있어도 동작할 수 있다. + +반면 서버 사이드 렌더링은 사용자의 요청을 받아 페이지를 렌더링할 서버가 필요하다. + +즉, 서버 운영, 배포, 캐싱, 장애 대응 등을 함께 고려해야 한다. + +## 3. 서비스 지연에 따른 문제가 생길 수 있다 + +서버 사이드 렌더링은 서버에서 렌더링이 완료된 뒤 HTML을 전달한다. + +따라서 최초 렌더링 과정에서 지연이 발생하면, 렌더링이 끝나기 전까지 사용자가 빈 화면을 보게 될 수 있다. + +특히 서버에서 처리해야 할 데이터 요청이 느리거나, 렌더링 작업이 무거우면 초기 응답 시간이 길어질 수 있다. + +--- + +# Next.js + +Next.js는 React 기반 프레임워크로, SPA 방식의 클라이언트 렌더링뿐만 아니라 서버 사이드 렌더링, 정적 사이트 생성, API Route, 파일 기반 라우팅 등을 제공한다. + +최초 진입 시 서버에서 미리 렌더링된 HTML을 제공할 수 있고, 이후 Hydration이 완료되면 브라우저의 JavaScript를 바탕으로 SPA처럼 페이지 전환을 처리할 수 있다. + +즉, 초기 로딩은 서버 렌더링의 장점을 활용하고, 이후 사용자 인터랙션과 라우팅은 클라이언트 측 JavaScript를 활용하는 구조라고 볼 수 있다. + +--- + +## swcMinify + +책에서는 `next.config.js`에 다음과 같이 작성하는 예시가 나온다. + +> 도서 기준 297페이지 + +```js +// next.config.js +module.exports = { + swcMinify: true, +}; +``` + +하지만 최신 Next.js에서는 이 설정을 직접 작성하지 않아도 된다. + +Next.js는 SWC 기반 minification을 기본으로 사용한다. + +또한 Next.js 15부터는 `next.config.js`에서 `swcMinify` 옵션을 설정할 수 없고, 해당 옵션은 제거되었다. + +따라서 최신 버전에서는 `swcMinify: true;` 옵션을 설정할 필요는 없다. + +출처: [공식 문서](https://nextjs.org/docs/architecture/nextjs-compiler?utm_source=chatgpt.com#minification) + + +--- + +# App Router의 404 처리 + +App Router에서는 `app/not-found.tsx` 파일을 만들면 404 페이지를 커스텀할 수 있다. + +```tsx +// app/not-found.tsx + +export default function NotFound() { + return
페이지를 찾을 수 없습니다.
; +} +``` + +특정 페이지에서 의도적으로 404를 띄우고 싶다면 `notFound()` 함수를 호출하면 된다. + +```tsx +import { notFound } from 'next/navigation'; + +export default async function Page() { + const data = await getData(); + + if (!data) { + notFound(); + } + + return
{data.title}
; +} +``` + +--- + +# App Router의 에러 처리 + +App Router에서 일반적인 런타임 에러는 `error.tsx`로 처리한다. + +```tsx +// app/error.tsx + +'use client'; + +export default function Error() { + return
문제가 발생했습니다.
; +} +``` + +`error.tsx`는 해당 route segment에서 발생한 에러를 처리하는 Error Boundary 역할을 한다. + +예상치 못한 에러나 500 에러에 가까운 상황도 `error.tsx`를 통해 사용자에게 별도의 에러 화면을 보여줄 수 있다. + +루트 레이아웃까지 포함한 전역 에러를 처리하고 싶다면 `global-error.tsx`를 사용할 수 있다. + +--- + +# getServerSideProps란? + +`getServerSideProps`는 Pages Router에서 사용하는 서버 사이드 렌더링 함수다. + +페이지 요청이 들어올 때마다 서버에서 데이터를 가져오고, 그 데이터를 기반으로 HTML을 생성한다. + +```tsx +// pages/posts/[id].tsx + +export async function getServerSideProps() { + const data = await fetchData(); + + return { + props: { + data, + }, + }; +} +``` + +즉, `getServerSideProps`는 요청 시점에 필요한 데이터를 서버에서 가져와 페이지 컴포넌트에 props로 전달하는 역할을 한다. + +--- + +# App Router에서 getServerSideProps의 변화 + +App Router에서는 `getServerSideProps`를 사용하지 않는다. + +대신 Server Component에서 `async` 함수를 사용해 직접 데이터를 가져올 수 있다. + +```tsx +// app/posts/[id]/page.tsx + +export default async function Page() { + const data = await fetchData(); + + return
{data.title}
; +} +``` + +Pages Router에서는 서버 데이터를 가져오기 위해 `getServerSideProps` 같은 별도의 함수를 사용했다. + +하지만 App Router에서는 기본적으로 컴포넌트가 Server Component이기 때문에, 컴포넌트 안에서 직접 `await`를 사용할 수 있다. + +--- + +# pages/api와 Route Handler + +Pages Router에서는 `pages/api` 폴더 안에 API Route를 만들 수 있다. + +```ts +// pages/api/hello.ts + +export default function handler(req, res) { + res.status(200).json({ message: 'hello' }); +} +``` + +`pages/api`는 React 페이지를 반환하는 것이 아니라, API 응답을 반환하는 서버 함수다. + +App Router에서는 `pages/api` 대신 Route Handler를 사용한다. + +Route Handler는 `route.ts` 파일로 작성한다. + +```ts +// app/api/hello/route.ts + +export async function GET() { + return Response.json({ message: 'hello' }); +} +``` + +즉, App Router에서는 API 응답을 만들고 싶을 때 `app/api/.../route.ts` 파일을 사용한다. + +--- + +# getStaticPaths + +`getStaticPaths`는 Pages Router에서 동적 라우트의 어떤 URL을 빌드 시점에 미리 만들지 정하는 함수다. + +예를 들어 파일이 다음과 같이 있다고 해보자. + +```tsx +pages/posts/[id].tsx +``` + +이 파일 하나로 여러 URL이 만들어질 수 있다. + +```txt +/posts/1 +/posts/2 +/posts/3 +``` + +그런데 Next.js 입장에서는 빌드 시점에 어떤 `id` 페이지를 미리 만들어야 하는지 알 수 없다. + +그래서 `getStaticPaths`로 미리 생성할 경로를 알려줘야 한다. + +```tsx +export async function getStaticPaths() { + return { + paths: [ + { params: { id: '1' } }, + { params: { id: '2' } }, + { params: { id: '3' } }, + ], + fallback: false, + }; +} +``` + +App Router에서는 `getStaticPaths`를 사용하지 않는다. + +--- + +# generateStaticParams + +App Router에서는 `getStaticPaths` 대신 `generateStaticParams`를 사용한다. + +`generateStaticParams`는 동적 라우트에서 어떤 URL을 빌드 시점에 미리 생성할지 정하는 함수다. + +예를 들어 App Router에서 파일 구조가 다음과 같다고 해보자. + +```tsx +app/posts/[id]/page.tsx +``` + +이때 `/posts/1`, `/posts/2`, `/posts/3` 페이지를 미리 만들고 싶다면 다음과 같이 작성할 수 있다. + +```tsx +export async function generateStaticParams() { + return [ + { id: '1' }, + { id: '2' }, + { id: '3' }, + ]; +} + +export default function Page({ params }: { params: { id: string } }) { + return
게시글 ID: {params.id}
; +} +``` + +`generateStaticParams`는 Pages Router의 `getStaticPaths`와 비슷한 역할을 한다. + +다만 App Router에서는 `paths` 객체를 반환하지 않고, params 객체 배열을 반환한다. + +--- + +# getStaticProps + +`getStaticProps`는 Pages Router에서 정적 페이지 생성을 위해 사용하는 함수다. + +빌드 시점에 데이터를 가져오고, 그 데이터를 기반으로 HTML을 미리 만들어둔다. + +```tsx +export async function getStaticProps() { + const data = await fetchData(); + + return { + props: { + data, + }, + }; +} +``` + +`getStaticProps`는 빌드 시점에 실행되기 때문에, 자주 바뀌지 않는 데이터를 가진 페이지에 적합하다. + +App Router에서는 `getStaticProps`를 사용하지 않는다. + +대신 Server Component의 `fetch` 옵션이나 route segment config를 통해 정적 생성, 캐싱, 재검증 방식을 제어한다. + +--- + +# Server Component에서 데이터 캐싱 제어 + +App Router에서는 Server Component에서 데이터를 가져올 때 `fetch` 옵션을 사용해 캐싱 방식을 제어할 수 있다. + +## revalidate + +```tsx +const res = await fetch('https://example.com/api/posts', { + next: { + revalidate: 60, + }, +}); +``` + +`revalidate: 60`은 데이터를 60초 동안 캐싱하고, 이후 다시 요청이 들어오면 데이터를 재검증할 수 있게 한다. + +즉, 정적 페이지처럼 동작하면서도 일정 시간이 지나면 데이터를 새로 갱신할 수 있다. + +## cache: 'no-store' + +```tsx +const res = await fetch('https://example.com/api/posts', { + cache: 'no-store', +}); +``` + +`cache: 'no-store'`는 요청 결과를 캐싱하지 않고, 매 요청마다 새 데이터를 가져오도록 한다. + +즉, Pages Router의 `getServerSideProps`처럼 요청 시점의 최신 데이터가 필요한 경우에 사용할 수 있다. + +--- + +# Pages Router와 App Router 비교 + +| 개념 | Pages Router | App Router | +| --- | --- | --- | +| 페이지 위치 | `pages` | `app` | +| API 작성 | `pages/api` | `app/api/.../route.ts` | +| 404 페이지 | `pages/404.tsx` | `app/not-found.tsx` | +| 에러 페이지 | `pages/500.tsx`, `_error.tsx` | `error.tsx`, `global-error.tsx` | +| 서버 사이드 렌더링 | `getServerSideProps` | Server Component + async/await | +| 정적 데이터 생성 | `getStaticProps` | Server Component + fetch cache | +| 동적 정적 경로 생성 | `getStaticPaths` | `generateStaticParams` | + +---