상태 관리 도구
프롭스 드릴링 문제를 Context로 해결하고 서버 상태·전역 상태의 성격에 따라 TanStack Query와 상태 도구를 선택합니다.
앞의 글들에서는 useState와 props를 이용한 기본 상태 관리, 그리고 서버 컴포넌트와 클라이언트 컴포넌트 사이의 폼 처리 흐름을 살펴봤습니다. 하지만 애플리케이션이 커질수록 useState와 props만으로는 한계에 부딪히는 구간이 생깁니다. 트리가 깊어질 때 생기는 프롭 드릴링(prop drilling)과 컴포넌트 사이에서 상태를 함께 쓰는 복잡함이 대표적입니다. 이런 문제를 해결하려고 React 생태계에서는 여러 상태 관리 도구가 발전해 왔으며, Next.js App Router에서는 이 도구들을 클라이언트 컴포넌트 안에서 활용해 전역 상태를 관리할 수 있습니다. 이 글에서는 React의 내장 Context API와 널리 쓰이는 외부 상태 관리 라이브러리를 살펴봅니다.
React Context API
React Context API는 props를 명시적으로 넘기지 않고도 컴포넌트 트리를 가로질러 데이터를 나눌 수 있게 해 주는 React의 내장 기능입니다. 로그인한 사용자 정보, 다크 모드와 라이트 모드 같은 테마, 언어 설정처럼 여러 곳에서 접근해야 하는 데이터에 특히 유용합니다.
Context API는 세 부분으로 동작합니다. createContext는 값을 넘길 Context 객체와 기본값을 정의하며, 상태 자체는 별도의 useState나 useReducer 등이 보관합니다. Context 객체의 Provider 컴포넌트는 Context 값을 설정하고, 그 아래의 클라이언트 소비자는 같은 Context에서 가장 가까운 Provider의 값을 읽습니다. 서버 컴포넌트 자체는 이 클라이언트 Context를 소비하지 않습니다. 마지막으로 자식 컴포넌트는 useContext 훅으로 Provider가 제공한 값을 읽습니다.
Context API를 쓰면 여러 단계를 거쳐 props를 넘길 필요 없이, 필요한 컴포넌트에서 바로 전역 상태에 접근할 수 있어 프롭 드릴링이 해결됩니다. React에 들어 있는 기능이므로 별도의 라이브러리를 설치할 필요도 없습니다. 대신 Context 값이 바뀌면 그 Context를 쓰는 모든 컴포넌트가 다시 렌더링될 수 있어, 불필요한 다시 렌더링이 생기고 최적화가 필요할 수 있습니다. 또한 Context는 값을 전달하는 역할만 하므로, 복잡한 상태 전환은 useReducer 등과 함께 구성하며, 미들웨어나 구독 최적화가 필요하다면 외부 도구와 비교해 봅니다.
실습: 테마(다크/라이트 모드) 변경 예제
간단한 테마 토글 기능을 Context API로 구현해 보겠습니다. 먼저 클라이언트 컴포넌트인 src/app/context-api/ThemeContext.tsx에서 Context를 정의하고 Provider 컴포넌트를 만듭니다.
"use client";
import React, { createContext, useContext, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
// 1. Context 객체 생성 (기본값 설정)
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
}
// 2. Provider 컴포넌트 정의
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. useContext 훅을 간편하게 사용할 커스텀 훅
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}createContext의 기본값을 undefined로 두었으므로, ThemeProvider 바깥에서 useTheme()를 부르면 context가 undefined가 되어 바로 오류를 던집니다. 이 검사 덕분에 Provider로 감싸는 것을 잊었을 때 조용히 잘못된 값을 쓰지 않고 원인이 드러나며, 검사를 통과한 뒤의 반환값은 ThemeContextType으로 좁혀집니다. 다음으로 Context 값을 쓰는 클라이언트 컴포넌트인 src/app/context-api/ThemeToggler.tsx를 만듭니다. 이 버튼은 useTheme()로 받은 theme으로 자기 색과 글자를 정하고, 누르면 toggleTheme으로 Provider의 상태를 바꿉니다.
"use client";
import React from 'react';
import { useTheme } from './ThemeContext';
export default function ThemeToggler() {
const { theme, toggleTheme } = useTheme(); // useTheme 훅으로 Context 값 접근
return (
<button
onClick={toggleTheme}
style={{
padding: '10px 20px',
fontSize: '1em',
backgroundColor: theme === 'dark' ? '#555' : '#eee',
color: theme === 'dark' ? 'white' : '#333',
border: `1px solid ${theme === 'dark' ? '#777' : '#ccc'}`,
borderRadius: '5px',
cursor: 'pointer',
transition: 'all 0.3s ease',
}}
>
현재 테마: {theme === 'light' ? '밝음 ☀️' : '어두움 🌙'}
</button>
);
}ThemeToggler는 테마 값을 props로 받지 않습니다. Provider의 상태가 바뀌면 useTheme()로 그 Context를 읽는 컴포넌트가 모두 다시 렌더링되므로, 버튼과 아래 패널이 같은 값으로 함께 바뀝니다. src/app/context-api/ThemePanel.tsx는 Context의 theme 값을 실제 DOM의 data-theme 속성과 패널 색상에 반영하는 클라이언트 컴포넌트입니다.
"use client";
import { useTheme } from './ThemeContext';
import ThemeToggler from './ThemeToggler';
export default function ThemePanel() {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<section
data-theme={theme}
style={{
padding: '20px',
maxWidth: '600px',
margin: '20px auto',
border: `1px solid ${isDark ? '#555' : '#ccc'}`,
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '20px',
backgroundColor: isDark ? '#222' : '#fff',
color: isDark ? '#f0f0f0' : '#222',
transition: 'background-color 0.2s ease, color 0.2s ease',
}}
>
<h1>Context API를 이용한 테마 변경</h1>
<p>Provider의 상태를 읽는 패널이 실제 화면 테마를 적용합니다.</p>
<ThemeToggler />
<p>이 텍스트와 패널 배경은 테마에 따라 변경됩니다.</p>
</section>
);
}마지막으로 서버 컴포넌트인 src/app/context-api/page.tsx에서 ThemeProvider로 클라이언트 컴포넌트들을 감쌉니다. 서버 페이지는 Provider와 패널을 조합할 뿐이며, 상태를 읽어 DOM을 바꾸는 코드는 ThemePanel 클라이언트 컴포넌트가 맡습니다.
import { ThemeProvider } from './ThemeContext'; // 클라이언트 Provider 임포트
import ThemePanel from './ThemePanel';
export default function ContextApiPage() {
return (
<ThemeProvider>
<ThemePanel />
</ThemeProvider>
);
}http://localhost:3000/context-api에 접속해 "현재 테마: 밝음" 버튼을 눌러 보세요. 버튼뿐 아니라 패널의 data-theme, 배경색, 테두리와 글자 색상이 함께 토글됩니다.
NEXT · CH8-4
theme 상태는 Provider에, 화면 반영은 소비자에 둡니다
선은 부모가 자식을 렌더링하는 관계입니다. ThemeProvider가 useState의 theme과 toggleTheme을 제공하고, ThemePanel·ThemeToggler가 useTheme으로 읽습니다.
서버 page는 Client Context를 소비하지 않습니다. ThemePanel은 data-theme·패널 색상을, ThemeToggler는 버튼과 토글을 담당합니다. 새 value 객체와 소비자 갱신 비용은 실제 측정하며 Context 추가만으로 렌더 횟수가 줄지는 않습니다.
위 그림은 테마 예제의 렌더링 트리와 Context 소비자를 보여 주며, 선은 부모가 자식을 렌더링하는 관계입니다. 서버 페이지가 ThemeProvider를 렌더링하고, 그 아래에 ThemePanel과 ThemeToggler가 차례로 놓입니다. ThemeProvider는 useState의 theme과 toggleTheme을 제공하고, 두 클라이언트 소비자가 useTheme으로 값을 읽습니다. 그림의 설명처럼 서버 페이지는 클라이언트 Context를 소비하지 않으며, ThemePanel은 data-theme과 패널 색상을, ThemeToggler는 버튼과 토글을 맡습니다.
처음 theme은 light입니다. ThemeToggler의 버튼을 눌러 toggleTheme을 실행하면 ThemeProvider 안의 업데이트 함수가 light를 dark로 바꾸고 새 value를 제공합니다. ThemePanel은 그 값으로 data-theme과 패널 색상을, ThemeToggler는 버튼 문구와 색상을 바꿉니다. 서버 페이지가 useTheme으로 이 값을 읽는 단계는 없습니다. Context는 값이 전달되는 경로이고, theme 상태는 Provider 안의 useState가 보관합니다.
테마 값과 value 객체의 정체성 구분하기
이 예제는 렌더링할 때마다 새 value 객체를 만듭니다. theme이 계속 "light"여도 { theme, toggleTheme }을 다시 만들면 이전 객체와 새 객체는 서로 다릅니다. React는 Provider의 이전 value와 다음 value를 Object.is로 비교하므로, 테마 문자열이 같다는 이유만으로 Context 갱신이 생략되지는 않습니다. 소비자를 memo로 감싸도 새 Context 값을 받는 갱신까지 막지는 못합니다.
필요하다면 상태와 구독 범위를 나누거나 함수와 value의 참조를 안정화하는 방식을 비교하되, 실제 비용이 없는 작은 예제에 최적화부터 덧붙일 필요는 없습니다. 부모의 다시 렌더링까지 고려한 구독 비용은 실제로 측정하고 최적화하며, Context를 더했다는 이유만으로 렌더링 횟수가 줄지는 않습니다.
중첩한 Provider도 상태를 합치지 않습니다. 바깥 Provider가 light를, 안쪽 Provider가 dark를 제공하면 안쪽의 ThemePanel은 가장 가까운 안쪽 값을 읽습니다. 같은 Context 객체는 값을 찾는 경로를 정할 뿐이며, 각 Provider 인스턴스의 상태 수명은 따로입니다.
외부 상태 관리 라이브러리
Context API는 간단한 전역 상태 관리에 알맞지만, 복잡한 업무 로직이나 비동기 상태가 많아지면 외부 상태 관리 라이브러리를 검토합니다. 이런 라이브러리의 React 상태 훅은 클라이언트 컴포넌트에서 씁니다. 다만 TanStack Query처럼 서버의 프리패치와 하이드레이션을 지원하는 도구도 있으므로, 모든 준비 작업이 브라우저에서만 일어난다는 뜻은 아닙니다. 널리 쓰이는 라이브러리를 몇 가지 살펴보겠습니다.
TanStack Query (React Query)
TanStack Query(이전 이름 React Query)는 주로 서버 상태(server state) 관리에 특화된 라이브러리입니다. fetch 등으로 서버에서 가져오는 비동기 데이터를 캐싱하고, 동기화하고, 갱신하는 과정을 효율적으로 관리해 줍니다. 로딩, 오류, 성공 같은 데이터 로딩 상태와 다시 가져오기(refetching), 변경(mutation)을 처리해 주어 개발자가 데이터 페칭 로직에 덜 신경 쓰게 돕습니다.
TanStack Query는 캐시된 데이터를 먼저 보여 주면서 백그라운드에서 최신 데이터를 가져와 UI를 갱신합니다. isLoading, isError, data 같은 훅의 반환값으로 로딩, 오류, 성공 상태의 UI를 쉽게 구성할 수 있습니다. POST, PUT, DELETE 같은 서버 데이터 변경을 위한 뮤테이션 기능을 제공하며, 변경한 뒤 onSuccess 등에서 invalidateQueries나 setQueryData를 명시적으로 불러 관련 캐시를 갱신합니다. React Query Devtools로 캐시 상태를 눈으로 확인하며 디버깅할 수도 있습니다. REST API 등 서버에서 데이터를 가져와야 할 때, 복잡한 캐싱 전략이 필요할 때, 로딩과 오류와 성공 상태를 일관되게 관리해야 할 때, 데이터를 바꾼 뒤 UI가 자동으로 갱신되는 것이 중요할 때 알맞습니다.
다만 서버 변경과 조회 캐시의 갱신은 서로 다른 단계입니다. 재고 조회의 캐시가 10을 표시하고, 서버 변경 요청이 성공해 DB 값이 9가 되었다고 합시다. 이 성공만으로 기존 조회 캐시의 값이 반드시 9가 되지는 않습니다. 성공한 뒤 해당 조회 키를 무효화해 다시 읽거나, 응답의 새 값을 알맞은 캐시에 명시적으로 반영해야 합니다. TanStack Query의 invalidateQueries는 일치하는 조회를 오래된 것으로 표시하고 기본적으로 활성 조회를 다시 읽게 할 수 있지만, 비활성이거나 사용이 꺼진 조회 등은 조건이 다릅니다. 다시 조회가 끝나기 전의 이전 값과 로딩, 오류 상태도 화면에서 다루어야 합니다. 로컬 상태 도구를 바꿨다는 사실만으로 서버 변경과 캐시 동기화가 완성되지는 않습니다.
Zustand / Jotai
Zustand와 Jotai는 각각 플럭스(Flux) 패턴과 아톰(atom)에 기반한 가벼운 상태 관리 도구입니다. 선택자(selector), 상태 단위, 구독 방식에 따라 필요한 값만 따로 구독할 수 있고, 상태를 간결하게 함께 쓰는 구성을 제공합니다. 같은 화면을 Context로 만든 것보다 항상 빠르다고 결론 내리기보다는, 구독 범위와 상태 변경 패턴을 정해 실제 갱신 비용을 비교합니다.
Zustand는 작고 간결한 훅 기반 store를 제공하며, 필요한 상태 조각을 selector로 구독합니다. Redux DevTools나 persist 같은 미들웨어를 골라 붙일 수 있습니다. Jotai는 작은 상태 단위인 atom을 조합하는 모델을 쓰며, 서로 독립적인 상태와 파생 상태가 많은 화면에 알맞습니다.
두 도구는 선택한 상태 조각이나 atom에 필요한 구독만 연결합니다. 다만 선택한 값의 비교 방식, 파생 상태, 부모의 렌더링도 영향을 주므로, 해당 값이 바뀐 소비자만 언제나 렌더링된다거나 렌더링 횟수가 최소라고 보장된다고 해석하지 않습니다. useState와 비슷한 훅 기반 API를 제공해 배우기 쉬우며, 같은 store 인스턴스나 Provider 범위에 연결된 소비자가 상태를 함께 씁니다. 다른 Provider, 다른 store, 서버 컴포넌트까지 자동으로 같은 값을 읽는 것은 아닙니다. UI 테마, 장바구니, 필터 설정처럼 클라이언트에서만 관리하는 전역 상태가 많을 때, Context API의 다시 렌더링 성능이 문제가 될 때, Redux의 복잡함이 부담스러울 때 검토합니다.
한때 많이 쓰이던 Recoil은 Recoil 공식 저장소가 2025년 1월부터 읽기 전용으로 보관되었으므로, 새로 도입하기 전에 유지보수 상태를 확인해야 합니다. 기존 Recoil 프로젝트는 사용 범위와 React 호환성을 확인하고, 전환이 필요하다면 비용과 대안을 비교합니다.
Redux Toolkit
Redux Toolkit은 Redux를 더 쉽게 쓰도록 만든 공식 권장 도구 모음입니다. Flux 구조와 예측 가능한 상태 관리라는 Redux의 핵심 개념은 그대로 따르면서, 반복되는 상용구 코드를 크게 줄이고 개발 경험을 높였습니다.
Redux Toolkit은 예측할 수 있고 디버깅하기 쉬운 형태로 상태 변경 로직을 쓰게 합니다. Redux Thunk나 Redux Saga 같은 비동기 작업, 로깅, 분석 같은 추가 기능을 미들웨어로 통합할 수 있고, Redux DevTools로 상태 변화를 시간 순서대로 추적하며 디버깅할 수 있습니다. 상태 관리 로직이 복잡하고 넓은 대규모 애플리케이션, 명확한 상태 변경 로직과 중앙 집중식 상태 관리가 필요한 경우, Redux 생태계의 여러 미들웨어와 확장 도구를 쓰고 싶을 때 알맞습니다.
App Router에서의 상태 관리 전략 선택
Next.js App Router에서는 서버 컴포넌트와 클라이언트 컴포넌트의 역할 분담을 생각해 상태 관리 도구를 고르는 것이 중요합니다.
서버 컴포넌트는 상호작용 상태를 useState로 보관하거나 useEffect로 관리하지 않습니다. 그렇다고 요청을 처리하는 동안의 지역 변수나 계산, 서버 데이터 읽기까지 금지되는 것은 아니며, 데이터 페칭은 async/await로 서버에서 직접 합니다. 레이아웃과 페이지는 필요한 데이터를 props로 넘기거나 클라이언트 Provider에 초기값을 줄 수 있습니다. 클라이언트 Provider가 서버에서 만든 children을 감싸더라도, 서버 컴포넌트에 Context 값을 넘기는 것은 아닙니다. 클라이언트 컴포넌트는 사용자 상호작용에 따른 동적인 상태 변화를 맡으며, useState, useContext나 TanStack Query, Zustand, Jotai, Redux Toolkit 같은 외부 라이브러리로 클라이언트 쪽 상태를 관리합니다.
브라우저에서 함께 쓰는 상태와 서버 프로세스의 전역 값은 구분해야 합니다. App Router의 초기 서버 렌더링에서는 여러 사용자의 요청이 같은 서버 프로세스를 쓸 수 있습니다. 사용자별 초기값을 모듈에 하나뿐인 store에 저장하면 다른 요청의 값과 섞일 수 있으므로, SSR에 쓰는 store와 조회 클라이언트는 요청마다 범위를 정합니다. 브라우저에서는 필요한 소비자가 같은 인스턴스를 함께 쓰고, 서버 HTML과 첫 클라이언트 출력의 초기 데이터를 맞춥니다. 서버 컴포넌트가 사용자별 전역 UI store를 읽거나 고치도록 만들지 않으며, 클라이언트의 사용자 정보나 장바구니 상태는 화면용 정보일 뿐이므로 서버 작업의 인증과 소유권 검사는 따로 합니다.
도구를 고르기 전에 상태의 원본, 함께 쓰는 범위, 최신성 요구를 먼저 정합니다. 입력 중인 값은 로컬 상태로 충분할 수 있고, 서버에서 가져온 데이터의 캐시와 브라우저의 UI 상태는 서로 다른 역할을 합니다. Context의 소비 범위는 React useContext, 변경 뒤의 캐시 갱신은 TanStack Query mutation 무효화, 서버에서의 준비는 서버 렌더링 안내에서 확인할 수 있습니다.
정리
- Context API는
createContext,Provider,useContext로 props 없이 트리 아래로 값을 넘기며, 상태 자체는Provider안의useState등이 보관합니다. - 소비자는 같은 Context에서 가장 가까운
Provider의 값을 읽고,Provider인스턴스마다 상태가 따로 있으며, 서버 컴포넌트는 클라이언트 Context를 소비하지 않습니다. Provider의value는Object.is로 비교되므로, 렌더링마다 새 객체를 만들면 소비자가 갱신되며memo로는 막을 수 없습니다.- TanStack Query는 서버 상태의 캐싱과 재조회를, Zustand와 Jotai는 가벼운 클라이언트 전역 상태를, Redux Toolkit은 대규모의 정형화된 상태 관리를 맡기기에 알맞습니다.
- 서버 변경이 성공해도 조회 캐시는 무효화나 명시적 반영을 거쳐야 갱신되며, SSR에 쓰는 store는 요청마다 범위를 나눕니다.
연습 문제
1. Provider를 두 개 두면 공유될까요?
같은 페이지에서 서로 형제인 ThemeProvider A와 B가 각자 ThemePanel을 하나씩 감싼다고 가정합니다. 둘 다 원래 코드의 light에서 시작하고, A 안의 버튼만 한 번 누릅니다. A와 B의 패널 색상은 각각 무엇일까요? B를 지우지 않은 채 A 바깥으로 옮긴 ThemeToggler가 어느 값을 읽을지도, 그 위치 위에 다른 ThemeProvider가 없다는 조건에서 설명해 보세요.
해설 보기
각 ThemeProvider 인스턴스가 별도의 useState를 가지므로 A는 dark, B는 light입니다. 같은 Context 객체를 쓴다는 이유만으로 두 Provider의 상태가 하나가 되지는 않으며, 각 소비자는 자신 위에 있는 가장 가까운 Provider의 값을 읽습니다.
Provider 밖의 ThemeToggler는 기본값 undefined를 읽고, useTheme의 검사에서 오류를 던집니다. 자동으로 A나 B를 찾아 가져오지 않습니다. 같은 값을 함께 쓰고 싶다면 필요한 소비자를 하나의 공통 Provider 아래에 둡니다. Provider를 없앴다가 다시 마운트하면 원래 코드의 light에서 시작하며, 이 예제에는 저장된 테마를 복원하는 기능이 없습니다.
2. 중첩한 Provider의 클릭
같은 Context로 만든 바깥 Provider A와 그 안의 Provider B가 모두 light에서 시작합니다. B의 버튼을 한 번, A에 직접 속한 버튼을 두 번 누릅니다. B 안의 ThemePanel은 어느 테마를 읽을까요? 각 클릭은 해당 Provider의 toggleTheme만 실행한다고 가정합니다.
해설 보기
B는 light에서 dark가 되고, A는 light에서 dark를 거쳐 다시 light로 돌아옵니다. B 안의 패널은 가장 가까운 B의 dark를 읽습니다. A의 상태 전환이 B의 상태를 덮거나, B가 두 상태를 합치는 일은 없습니다.
3. 같은 테마, 새 value 객체
부모가 다시 렌더링되어 ThemeProvider가 새 { theme, toggleTheme }을 만들었습니다. theme은 여전히 light입니다. 이전과 다음 value 객체를 Object.is로 비교하면 같을까요? 소비자의 memo만으로 Context 값의 갱신을 막을 수 있을까요?
해설 보기
같지 않습니다. 새 객체는 이전 객체와 같은 참조가 아니므로 Object.is는 false입니다. Context 소비자는 새 value를 받으며, memo만으로는 이를 막지 못합니다. 테마 문자열이 같은 것과 value 객체가 같은 것은 구분해야 하며, 필요할 때 실제 갱신 비용에 맞춰 참조와 구독 범위를 검토합니다.
4. 서버 변경에 성공한 뒤의 조회 캐시
재고 조회 캐시에 10이 있습니다. 서버 변경은 성공해 DB를 9로 바꿨지만 조회 캐시를 갱신하지 않았고, 별도의 자동 재조회도 일어나지 않았습니다. 성공 메시지만으로 화면의 조회값이 9라고 할 수 있을까요? 필요한 단계를 설명해 보세요.
해설 보기
할 수 없습니다. 주어진 조건에서는 조회 캐시가 여전히 10일 수 있습니다. 성공한 뒤 관련 조회 키를 무효화해 다시 읽거나, 응답의 새 값을 캐시에 명시적으로 반영해야 합니다. 그리고 실제 재조회나 반영이 끝나 표시값이 9로 바뀌었는지까지 확인합니다. 서버 변경의 성공과 클라이언트 캐시의 동기화는 따로 확인해야 하는 일입니다.
5. 요청끼리 섞이는 store
SSR 요청 A와 B가 한 서버 프로세스의 사용자별 UI store를 함께 씁니다. A가 저장한 초기값을 B가 읽을 수 있는 구조라면 어떤 범위를 바꿔야 할까요? 브라우저 store에 로그인한 사용자 ID가 있다는 것만으로 서버 작업의 권한도 인정해도 될까요?
해설 보기
SSR에 쓰는 사용자별 store를 요청마다 따로 만들어, 초기 데이터가 다른 요청과 섞이지 않게 합니다. 브라우저에서 함께 쓸 범위는 그와 별도로 정합니다. 사용자 ID가 UI store에 있다는 사실은 인증을 확인해 주지 않으므로, 서버는 검증한 세션과 해당 작업의 권한을 직접 확인해야 합니다.
이것으로 8장을 마칩니다. 8장에서는 useState와 props로 시작해 서버 액션과 폼 처리, Context API와 외부 상태 관리 도구까지 상태를 다루는 방법을 정리했습니다. 다음 장의 CSS 모듈 활용부터는 Next.js 애플리케이션에 스타일을 입히는 방법을 알아봅니다.