이정환님의 '한 입 크기로 잘라먹는 Next.js(v15)' 강의를 듣고 정리한 내용입니다.
한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런
이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지💡 Page Router란?Next.js
www.inflearn.com
App Router: Next.js 13 버전에 새롭게 추가된 라우터
Page Router와 비교해서 네비게이팅, 프리페칭, 사전 렌더링은 크게 바뀌지 않는다.
1. App Router 버전의 페이지 라우팅
“page”라는 이름의 파일이 페이지 역할을 하는 파일로 자동 설정
~/book을 만드려면
book 폴더 안에 page.tsx를 만들면 된다.
동적 경로에 대응하려면?
book 폴더 안의 [id] 폴더를 생성하고 page.tsx를 만들면 된다.
쿼리 스트링, URL 파라미터와 같은 경로상에 포함되는 값들은 props에 전달이 된다.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q: string }>;
}) {
const { q } = await searchParams;
return <div>Search 페이지 {q}</div>;
}
URL 파라미터가 중첩으로 여러개 전달되는 경로에도 대응하려면?
→ 폴더명을 […id]로 바꾸자 (캐치 올 세그먼트)
URL 파라미터가 없을 때에도 대응하고 싶다면?
→ 폴더명을 [[…id]]로 만들자 (옵셔널 캐치 올 세그먼트)
2. 레이아웃
어떤 페이지의 레이아웃을 설정하고 싶으면 그 폴더 안에 layout.tsx를 생성하면 된다.
/search/layout.tsx를 만들면 → “/search” 경로로 시작하는 모든 페이지의 레이아웃으로 적용된다.
/search/layout.tsx와 /search/setting/layout.tsx가 있다면?
→ /search/layout.tsx이 적용되고, 그 안쪽으로 /search/setting/layout.tsx가 적용된다.
/src/app에 있는 layout.tsx은 루트 레이아웃
→ 절대 없어지면 안됨 (이름을 바꾸면 자동 생성될 정도)
레이아웃을 직접 생성할 때에는 Children이라는 props를 통해서 페이지 컴포넌트를 전달 받아서 return 문에 배치를 시켜야한다.(타입은 ReactNode)
경로와 관계없이 특정 페이지들에만 적용이 되는 공통 레이아웃을 적용하려면?
app 밑에 소괄호로 감싸진 폴더를 생성 (RouteGroup)
→ 경로상에 아무런 영향을 주지 않음
→ 각기 다른 경로를 갖는 페이지 파일들을 하나의 폴더 안에 묶어둘 수 있음
3. React Server Component
React 18v부터 추가된 새로운 유형의 컴포넌트
→ 서버측에서만 실행되는 컴포넌트 (브라우저에서 실행x)
→ 서버측에서 사전 렌더링을 진행할 때 딱 한번만 실행됨
(클라이언트 컴포넌트는 사진 렌더링 진행할 때 한번, hydration 진행할 때 한 번 = 총 2번 실행)
페이지의 대부분을 서버 컴포넌트로 구성할 것을 권장
→ 클라이언트 컴포넌트는 꼭 필요한 경우에만 사용
클라이언트 컴포넌트로 사용하려면 파일 최상단에 ‘use client’라는 지시자를 작성해야
참고: 앱 라우터에서는 파일의 이름이 Page나 Layout이 아닌 일반적인 파일을 생성해도 된다. (Co-Location)
주의사항1. 서버 컴포넌트에서는 브라우저에서 실행될 코드가 포함되면 안된다
→ React Hooks, 이벤트 핸들러, 브라우저에서 실행되는 기능을 담고 있는 라이브러리..
주의사항2. 클라이언트 컴포넌트는 클라이언트에서만 실행되지 않는다
→ 서버와 클라이언트에서 모두 실행된다
주의사항3. 클라이언트 컴포넌트에서 서버 컴포넌트를 import 할 수 없다
→ 이 경우 Next.js는 자동으로 서버 컴포넌트를 클라이언트 컴포넌트로 변경
→ 되도록이면 클라이언트 컴포넌트의 자식으로 서버 컴포넌트를 배치하는건 피하자
→ 써야된다면 import해서 쓰지 말고 children props를 받아서 전달하면 된다
export default function Home() {
return (
<div className={styles.page}>
<ClientComponent>
<ServerComponent />
</ClientComponent>
</div>
);
}
주의사항4. 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화되지 않는 props는 전달 불가하다
직렬화(serialization): 객체, 배열, 클래스 등의 복잡한 구조의 데이터를 네트워크 상으로 전송하기 위해 아주 단순한 형태(문자열, Byte)로 변환하는 것
→ 함수는 직렬화 불가
사전 렌더링 과정 진행 중일 때 사실 서버 컴포넌트들만 따로 실행되는 과정이 진행된다.
→ 이 때 RSC Payload 생성
RSC Payload: React Server Component의 순수한 데이터(결과물)
→ React Server Component를 직렬화한 결과
→ RSC Payload에는 서버 컴포넌트의 모든 데이터가 포함 (서버 컴포넌트의 렌더링 결과, 연결된 클라이언트 컴포넌트의 위치, 클라이언트 컴포넌트에게 전달하는 Props 값 등)
4. 네비게이팅
페이지 이동은 Client Side Rendering 방식으로 처리
서버에서 브라우저한테 JS Bundle을 전달할 때 RSC Payload도 같이 전달하게 된다
programmatic하게 이동하려면 useRouter를 import한다 ( next/navigation에서 가져와야한다)
import { useRouter } from "next/navigation";
export default function Searchbar() {
const router = useRouter();
const [search, setSearch] = useState("");
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const onSubmit = () => {
router.push(`/search?q=${search}`);
};
동적인 페이지는 일단 RSC 페이로드만 가져온다.
(정적인 페이지와 다르게 데이터 업데이트가 향후 필요할 수도 있으므로)
App Router 버전에서는 서버 컴포넌트는 RSC 페이로드로,
클라이언트 컴포넌트는 JS Bundle로 나뉘어서 동시에 전달로 된다.
App Router에서 쿼리스트링을 불러오려면 useSearchParams를 활용해야 한다.
const searchParams = useSearchParams();
const q = searchParams.get("q");
5. 데이터 페칭 in App router
클라이언트 컴포넌트에서는 Async 키워드를 사용할 수 없었음
→ 브라우저에서 동작 시 문제를 일으킬 수 있기 때문에 권장되지 않음
→ useMemo나 useCallback같은 memoization 차원에서 문제 발생 가능성
하지만 서버 컴포넌트는 브라우저에서 실행되지 않기 때문에 Async 키워드 사용이 가능하다
→ await 키워드로 비동기적으로 컴포넌트 내에서 직접 데이터 페칭 가능 (Fetching data where it’s needed)
→ 더 이상 페이지 컴포넌트로부터 데이터를 props나 context API로 넘겨줄 필요가 없다!
환경변수 설정히 ‘NEXT_PUBLIC’을 붙이지 않으면 서버 측에서만 접근이 가능하게 된다.
6. Data Cache
fetch 메서드를 활용해 불러온 데이터를 Next 서버에서 보관하는 기능
영구적으로 데이터를 보관하거나, 특정 시간을 주기로 갱신시키는 것도 가능
불필요한 데이터 요청의 수를 줄여서 웹 서비스의 성능을 크게 개선할 수 있음
const response = await fetch(~/api, { cache : “force-cache” });
cache: “force-cache”
→ 요청의 결과를 무조건 캐싱
→ 한번 호출된 이후에는 다시 호출되지 않음
Next.js에서의 데이터 캐시를 사용하기 위해서는 fetch 메서드를 사용해야 한다(axios같은 라이브러리 안됨)
cache: “no-store”
→ 데이터 페칭의 결과를 저장하지 않는 옵션
→ 캐싱을 아예 하지 않도록 설정하는 옵션
→ 캐싱 옵션으로 아무것도 주지 않으면 기본 값으로써 캐싱이 되지 않는 요청으로 동작한다. (15버전부터)
next.config.mjs 안에 아래 코드를 설정하면 모든 데이터 페칭이 로그로써 콘솔에 잘 출력된다.
const nextConfig = {
logging: {
fetches: {
fullUrl: true,
},
},
};
next: { revalidate: 3 }
→ 특정 시간을 주기로 캐시를 업데이트 함
→ 마치 Page Router의 ISR 방식과 유사함
→ 3초 이후에 데이터 페칭을 하게 되면 캐시된 데이터를 stale로 설정하고 캐시된 데이터를 반환, 그리고 서버에서 데이터 페칭을 진행해서 새로운 데이터 캐싱
next: { tags: [’a’] }
→ On-Demand Revalidate
→ 요청이 들어왔을 때 데이터를 최신화 함
7. Request Memoization
하나의 페이지를 렌더링하는 동안 중복된 API 요청을 캐싱하기 위해 존재
→ 렌더링이 종료되면 모든 캐시가 소멸
→ 자동으로 작동
데이터 캐시는?
→ 백엔드 서버로부터 불러온 데이터를 거의 영구적으로 보관하기 위해 사용
→ 서버 가동 중에는 영구적으로 보관
왜 만들어졌는가?
→ 서버 컴포넌트의 등장으로 데이터가 필요한 곳에서 직접 데이터를 페칭할 수 있게됨
→ 서로 다른 컴포넌트에서 중복된 api 요청이 생기는 일이 많아짐
8. Full Route Cache
Next 서버 측에서 빌드 타임에 특정 페이지의 렌더링 결과를 캐싱하는 기능
Dynamic Page로 설정되는 기준
→ 특정 페이지가 접속 요청을 받을 때마다 매번 변화가 생기거나, 데이터가 달라질 경우
(서버 컴포넌트만 해당, 클라이언트 컴포넌트는 페이지 유형에 영향을 미치지 않음)
- 캐시되지 않는 Data Fetching을 사용할 경우
- 동적 함수(쿠키, 헤더, 쿼리스트링)을 사용하는 컴포넌트가 있을 때
Static Page로 설정되는 기준
→ Dynamic Page가 아니면 모두 Static Page (Default)
Static Page에 Full Route Cache가 적용됨
(그렇다고 Dynamic Page를 지양해야한다는 것은 아님)
Full Route Cache 또한 revalidate가 가능하다
(데이터 캐시가 stale되므로 full route cache도 stale됨
즉, 단 하나의 컴포넌트라도 revalidate되면 Full Route Cache도 revalidate가 된다)
어떤 컴포넌트를 클라이언트 측에서만 실행이 되도록하고 싶다면?
(사전 렌더링 과정에서 배제가 되게 하고 싶다면?)
→ Suspense로 감싸고 fallback을 주자
<div>
<Suspense fallback={<div>Loading...</div>}>
<Searchbar />
</Suspense>
(...)
generateStaticParams 함수를 통해 정적인 파라미터를 생성할 수 있다.
export function generateStaticParams() {
return [{ id: "1" }, { id: "2" }, { id: "3" }];
}
위 함수를 통해 동적 경로를 갖는 페이지를 정적 페이지로 설정할 수 있다.
주의점1: 문자열로만 명시해야한다.
주의점2: 무조건 스태틱 페이지로 설정되니 주의 (페이지 캐싱을 하지 않는 설정이 있다고 하더라도)
export const dynamicParams = false; 옵션을 추가하면 동적 경로가 적용이 되지 않는다. (위의 1,2,3 경로빼고는 적용x)
9. Route Segment Option
특정 페이지의 유형을 강제로 Static, Dynamic 페이지로 설정
export const dynamic = ‘’
- auto: 기본값, 아무것도 강제하지 않음
- force-dynamic: 페이지를 강제로 Dynamic 페이지로 설정
- force-static: 페이지를 강제로 Static 페이지로 설정 (부작용 조심)
- error: 페이지를 강제로 Static 페이지 설정 (설정하면 안되는 이유 → 빌드 오류)
10. Client Router Cache
브라우저에 저장되는 캐시
페이지 이동을 효율적으로 진행하기 위해 페이지의 일부 데이터를 보관함
여러 개의 페이지가 공통된 레이아웃을 사용한다면, 중복된 RSC 페이로드를 여러 차례 브라우저에서 요청하거나 전달받게 된다는 문제 발생
→ 서버로부터 전달받게 되는 RSC 페이로드 값들 중 레이아웃에 해당하는 부분의 데이터만 따로 추출 후 Client Route Cache에 저장
→ 중복되는 레이아웃들을 Client Route Cache에 저장
→ 새로고침을 하거나 탭을 껐다 켰을 때는 적용
11. 스트리밍
잘개 쪼개진 데이터들을 연속적으로 보내주는 기술
→ 사용자에게 긴 로딩없이 좋은 경험 제공 가능
Next.js는 HTML 페이지를 스트리밍하는 기능을 자체적으로 제공
a. 페이지 스트리밍
오래 걸리는 컴포넌트의 렌더링을 사용자가 좀 더 좋은 환경에서 기다리게 할 수 있기 위해
→ 빨리 렌더링할 수 있는 컴포넌트 먼저 출력
→ 스트리밍은 Dynamic Page에 자주 사용됨
적용방법: 페이지가 있는 동일한 경로에 loading.tsx를 만들어주면 된다
(경로 아래에 있는 모든 페이지에 적용됨)
주의1: async 키워드가 붙어 비동기로 작동하도록 설정한 페이지 컴포넌트에만 스트리밍을 제공
주의2: loading.tsx에는 페이지 컴포넌트에만 적용
주의3: 브라우저에서 쿼리스트링이 변경될 때에는 스트리밍이 트리거되지 않는다
b. 컴포넌트 스트리밍
컴포넌트에도 스트리밍을 적용하고 싶다면, Suspense로 컴포넌트를 감싸면 된다.
쿼리스트링 변경 중에도 스트리밍을 적용하고 싶다면, key 값을 주면 된다 (key가 바뀔 때마다 컴포넌트가 바뀌므로)
<Suspense key={q || ""} fallback={<div>Loading ...</div>}>
<SearchResult q={q || ""} />
</Suspense>
);
c. 스켈레톤 UI
사용자가 로딩 시간부터 컴포넌트가 어떻게 생겼는지 예측할 수 있다
skeleton 컴포넌트를 만들고, 몇 개의 스켈레톤 리스트가 필요한지 전달받는 컴포넌트를 생성해서 사용하면 편하다.
import BookItemSkeleton from "./book-item-skeleton";
export default function BookListSkeleton({ count }: { count: number }) {
return new Array(count)
.fill(0)
.map((_, idx) => <BookItemSkeleton key={`book-item-skeleton-${idx}`} />);
}
React Loading Skeleton library를 사용해도 편하다.
d. 에러 핸들링
페이지와 같은 경로에 error.tsx를 생성하면 에러 발생 시 대체 UI를 생성할 수 있다.
(’use client’ 지시자를 추가해서 클라이언트 컴포넌트로 만들어주자 → 서버에서 발생하는 에러, 클라이언트에서 발생하는 에러 모두 대응하기 위해)
에러 컴포넌트의 props에 error를 활용하면 메시지를 출력할 수 있다. (type은 Error)
reset이라는 props를 활용해서 컴포넌트를 다시 렌더링을 시도할 수 있다. (type은 () ⇒ void )
→ 하지만 화면을 다시 렌더링하기만 할뿐, 서버 컴포넌트를 다시 실행하지는 않음
→ window.location.reload()를 사용해서 강제로 새로고침을 해도 좋다
→ 더 좋은 방법은 router.refresh() 사용 (현재 페이지에 필요한 서버 컴포넌트들을 다시 불러옴)
→ 그리고 그 뒤에 reset()을 사용해서 에러 상태를 초기화하고 컴포넌트들을 다시 렌더링해야한다.
그런데 router.refresh()는 비동기적으로 실행되므로 reset()이 먼저 실행되는 문제가 발생한다.
→ startTransition으로 감싸서 router.refresh()와 reset()이 일괄적으로 실행되도록 하자
"use client";
import { useRouter } from "next/navigation";
import { startTransition, useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
const router = useRouter();
useEffect(() => {
console.error(error.message);
}, [error]);
return (
<div>
<h3>오류가 발생했습니다</h3>
<button
onClick={() => {
// 함수 하나를 인수로받아서, 해당 함수 내부의 코드를 동기적으로 실행
startTransition(() => {
router.refresh(); // 현재 페이지에 필요한 서버컴포넌트들을 다시 불러옴
reset(); // 에러 상태를 초기화, 컴포넌트들을 다시 렌더링
});
}}
>
다시 시도
</button>
</div>
);
}
'Frontend > Next.js' 카테고리의 다른 글
[Next.js 강의 정리] 서버 액션, Parallel Route, 최적화에 관하여 (4) | 2025.06.08 |
---|---|
[Next.js 강의 정리] Next.js와 Page Router에 관하여 (0) | 2025.05.25 |
[Next.js] Next.js(SSR 환경)에서 css 애니메이션이 작동 안하는 이유 (1) | 2024.07.12 |