[Next.js/App Router] 6. App Router
App Router란?
Next.js 13버전부터 추가된 새로운 라우터 시스템으로, 기존 Page Router를 완전히 대체함.
✅ App Router 주요 변경 사항
- 데이터 페칭 방식 변경
- 레이아웃 설정 방식 변경
- 페이지 라우팅 설정 방식 변경
- React 18의 신규 기능 추가
- React Server Component (RSC)
- Streaming 지원
1) App Router 프로젝트 생성
npx create-next-app@latest section03
2) 페이지 라우팅 설정
- 기존 Page Router에서는 pages/ 디렉토리를 사용했지만, App Router에서는 app/ 디렉토리를 사용하여 페이지를 구성합니다.
3) 레이아웃 설정
App Router에서는 개별 페이지마다 레이아웃을 설정하는 방식이 아니라, 레이아웃을 공유할 수 있도록 구조화됩니다.
- layout.tsx 파일을 통해 공통 레이아웃 적용 가능
- 특정 페이지별 개별 레이아웃도 설정 가능
4) Route Group 설정
src/app/(이름) 구조를 사용하여 경로에 영향을 주지 않고 폴더를 그룹화할 수 있습니다.
- 위와 같이 (with-searchbar) 내부의 layout.tsx는 search/ 페이지와 index 페이지에서 동일하게 적용됩니다.
React Server Component (RSC)
React 18부터 새롭게 추가된 서버 전용 컴포넌트
1) 기존 Page Router의 문제점
Page Router에서는 모든 컴포넌트가 클라이언트 번들에 포함되어야 했습니다. 하지만 상호작용이 필요 없는 컴포넌트도 번들에 포함되면서 성능 저하가 발생하는 문제가 있었습니다.
- 불필요한 JavaScript 번들 크기 증가 → 브라우저에서 필요 없는 코드 로딩
- 하이드레이션 시간이 길어짐 → UI 렌더링 속도 저하
- TTI(Time to Interactive) 지연 → 사용자 경험 저하
2) RSC를 활용한 해결 방법
React Server Component를 활용하면, 상호작용이 필요 없는 컴포넌트는 서버에서만 실행되도록 최적화할 수 있습니다.
- 상호작용이 필요 없는 컴포넌트들은 서버에서만 실행되도록 RSC로 분리
- 브라우저에 전달되는 JS 번들에서 불필요한 컴포넌트 제외
- 서버에서 사전 렌더링하여 브라우저에서는 실행되지 않도록 최적화
💡
Next.js의 공식 문서에서도 가능한 대부분의 컴포넌트를 서버 컴포넌트로 만들 것을 권장하며, 클라이언트 컴포넌트는 필요한 경우에만 사용해야 함
3) RSC 특징
- 서버에서만 실행되며 브라우저에서 실행되지 않음
- 불필요한 JavaScript 번들을 줄여 성능 최적화 가능
- 상호작용이 필요 없는 컴포넌트를 서버에서 처리하여 번들 크기 감소
4) 서버 컴포넌트 vs 클라이언트 컴포넌트
✅ 어떤 컴포넌트가 서버 컴포넌트, 클라이언트 컴포넌트가 되어야할까?
- 상호작용이 있으면 클라이언트 컴포넌트, 없으면 서버 컴포넌트
- 헷갈리지 말것 : Link 기능은 HTML고유의 기능으로 상호작용이 아님
✅ 클라이언트 컴포넌트로 설정하려면?
- 상단에 "use client" 추가하면 클라이언트 컴포넌트로 변환됨
✅ React Server Component 주의사항
예시1) 서버 컴포넌트에는 브라우저에서 실행될 코드가 포함되면 안된다
예시2) 클라이언트 컴포넌트는 클라이언트에서만 실행되지 않는다
예시3) 클라이언트 컴포넌트에서 서버컴포넌트를 import 할 수 없다.
- Next는 클라이언트 컴포넌트에서 서버 컴포넌트를 import 하면 자동으로 클라이언트 컴포넌트로 변경해줌
- 클라이언트 컴포넌트에서 서버 컴포넌트를 import 해서 사용하는 것은 권장하지 않는다.
- 따라서 children props로 전달받아서 사용하는 방식 권장
import ClientComponent from "./client-component";
import styles from "./page.module.css";
import ServerComponent from "./server-component";
export default function Home() {
return (
<div className={styles.page}>
인덱스 페이지
<ClientComponent>
<ServerComponent />
</ClientComponent>
</div>
);
}
"use client";
import { ReactNode } from "react";
import ServerComponent from "./server-component";
// 클라이언트 컴포넌트에서 서버 컴포넌트를 import 해서 사용하는 것은 권장하지 않는다.
// 따라서 children props로 전달받아서 사용하는 방식 권장
export default function ClientComponent({ children }: { children: ReactNode }) {
console.log("클라이언트 컴포넌트");
return <div>{children}</div>;
}
export default function ServerComponent() {
console.log("서버 컴포넌트");
return <div></div>;
}
예시4) 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화되지 않는 Props는 전달 불가하다
5) Next.js 앱에서 서버 컴포넌트의 동작 방식
RSC Payload
React Server Component를 실행한 후 직렬화한 결과이며, 아래 정보를 포함합니다:
- 서버 컴포넌트의 모든 데이터
- 서버 컴포넌트의 렌더링 결과
- 연결된 클라이언트 컴포넌트의 위치
- 클라이언트 컴포넌트에게 전달하는 Props 값
서버 컴포넌트들은 먼저 실행되면서 RSC 페이로드 형태로 직렬화됩니다. 하지만 서버 컴포넌트에서 클라이언트 컴포넌트로 함수 형태의 값을 props로 전달하면 안 됩니다.
❌ 잘못된 예시
서버 컴포넌트에서 클라이언트 컴포넌트로 함수 값을 전달하면 직렬화 과정에서 오류가 발생합니다.
export default function ServerComponent() {
const handleClick = () => {
console.log("클릭됨");
};
return <ClientComponent onClick={handleClick} />; // ❌ 에러 발생
}
✅ 해결 방법
서버 컴포넌트에서 클라이언트 컴포넌트로 직렬화할 수 없는 값을 전달하지 않고, 클라이언트 컴포넌트 내부에서 직접 함수를 정의해야 합니다.
"use client";
export default function ClientComponent({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>클릭</button>;
}
Navigation (페이지 이동)
1) Link를 이용한 페이지 이동
App Router에서는 페이지 이동 시 JavaScript 번들과 함께 RSC Payload도 함께 전송됩니다.
// page.tsx
import ClientComponent from "../../components/client-component";
import styles from "./page.module.css";
import ServerComponent from "./server-component";
export default function Home() {
return (
<div className={styles.page}>
인덱스 페이지
<ClientComponent>
<ServerComponent />
</ClientComponent>
</div>
);
}
// search/page.tsx
import ClientComponent from "@/components/client-component";
export default async function Page({
searchParams,
}: {
searchParams: { q?: string };
}) {
const { q } = await searchParams;
return (
<div>
Search 페이지 {searchParams.q}
<ClientComponent>
<></>
</ClientComponent>
</div>
);
}
2) 프로그래매틱 페이지 이동
useRouter를 사용하여 페이지 이동을 직접 제어할 수 있습니다.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation"; // 앱라우터 버전인 next/navigation에서 import 해야함, next/router에서 import 하면 런타임 에러
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}`);
};
return (
<div>
<input type={search} onChange={onChangeSearch} />
<button onClick={onSubmit}>검색</button>
</div>
);
}
3) 프리페칭(Pre-Fetching)
App Router에서는 스태틱 페이지와 다이나믹 페이지를 구분하여 프리페칭을 최적화합니다.
- 스태틱 페이지: RSC 페이로드 + JS 번들
- 다이나믹 페이지: RSC 페이로드만 프리페칭
UI 설정하기
1. useSearchParams란?
useSearchParams는 Next.js의 next/navigation에서 제공하는 훅으로, 현재 페이지에 전달된 쿼리 스트링을 읽어올 수 있는 기능을 제공한다.
이를 활용하면 사용자가 입력한 검색어를 URL의 q 파라미터로 전달하고, 이를 다시 읽어와 상태를 유지할 수 있음
2. 검색 UI 구현하기
1) Searchbar 컴포넌트 생성
아래는 검색어 입력 후 URL에 반영하는 Searchbar 컴포넌트입니다.
"use client";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import style from "./searchbar.module.css";
export default function Searchbar() {
const router = useRouter();
const searchParams = useSearchParams(); // 현재 URL의 query string을 가져옴
const [search, setSearch] = useState("");
const q = searchParams.get("q"); // "q" 파라미터 값 가져오기
// URL에서 가져온 q 값을 input에 반영
useEffect(() => {
setSearch(q || "");
}, [q]);
// 입력값 변경 핸들러
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
// 검색 실행
const onSubmit = () => {
if (!search || q === search) return; // 빈 값이거나 기존 값과 동일하면 실행 X
router.push(`/search?q=${search}`); // 검색어를 URL에 반영
};
// 엔터 키 입력 시 검색 실행
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onSubmit();
}
};
return (
<div className={style.container}>
<input
value={search}
onChange={onChangeSearch}
onKeyDown={onKeyDown}
placeholder="검색어를 입력하세요"
/>
<button onClick={onSubmit}>검색</button>
</div>
);
}
2) search/page.tsx에서 검색 결과 가져오기
사용자가 검색어를 입력하면 /search?q=검색어 형태로 이동하므로, 검색 결과 페이지에서 이를 읽어와 사용할 수 있습니다.
import { useSearchParams } from "next/navigation";
export default function SearchPage() {
const searchParams = useSearchParams();
const query = searchParams.get("q"); // URL에서 "q" 값을 가져옴
return (
<div>
<h1>검색 결과</h1>
{query ? <p>"{query}"에 대한 검색 결과</p> : <p>검색어를 입력하세요.</p>}
</div>
);
}
3. 검색 UI의 동작 방식
- 사용자가 검색어를 입력하고 엔터 또는 버튼 클릭 → router.push("/search?q=검색어") 실행
- 검색 페이지(/search에서 useSearchParams.get("q"))를 통해 검색어 반영
- 검색 결과를 기반으로 데이터를 불러와 출력할 수 있음
🎯 배운점
✔ App Router는 Page Router를 대체하며, 더 강력한 기능과 유연성을 제공
✔ React Server Component를 활용하면 번들 크기를 줄이고 성능 최적화 가능
✔ 페이지 이동과 프리페칭이 더욱 최적화됨
✔ Next.js는 서버 컴포넌트 중심으로 개발하는 것을 권장하며, 클라이언트 컴포넌트는 필요한 경우에만 사용
✔ useSearchParams를 활용하면 Next.js에서 URL의 쿼리 스트링을 쉽게 읽고 반영하여 검색 상태를 관리할 수 있다.