유용한 Next.js 라이브러리 소개
인증·상태·UI·폼·데이터베이스 요구사항과 서버·클라이언트 실행 위치를 기준으로 보조 라이브러리를 선택합니다.
19장에서는 Next.js를 계속 공부하고 실무에 쓰는 데 도움이 되는 자료를 차례로 다룹니다. 먼저 자주 함께 쓰는 라이브러리를 살펴보고, 커뮤니티 리소스와 포럼, 공식 문서를 활용하는 방법으로 이어 갑니다.
Next.js는 React 기반 프레임워크이며, 추가 라이브러리는 기본 기능과 기존 의존성으로 해결하기 어려운 요구가 있을 때 검토합니다. Next.js 프로젝트에서는 인증, 상태 관리, UI, 데이터 페칭, 유효성 검사 같은 요구사항에 맞춰 외부 라이브러리를 함께 고르게 됩니다. 이 글에서는 Next.js 프로젝트에서 자주 쓰는 라이브러리를 카테고리별로 소개하고, 각 라이브러리의 주요 기능과 Next.js 환경에서의 활용 방법을 함께 정리합니다.
인증(Authentication)
사용자별 데이터나 보호된 기능이 있다면 로그인과 세션, 서버의 권한 검사를 설계해야 합니다. 인증 라이브러리는 로그인 화면, OAuth 콜백, 세션 쿠키 같은 공통 절차를 대신해 주지만, 어떤 사용자가 어떤 데이터를 바꿀 수 있는지는 애플리케이션이 정해야 합니다. 그래서 라이브러리를 고를 때는 지원하는 로그인 방식과 함께, 서버 컴포넌트, Route Handler, 서버 액션에서 세션을 얼마나 쉽게 읽을 수 있는지를 봅니다. 이 교재의 10장과 17장은 Auth.js를 썼습니다.
Auth.js
Auth.js는 Next.js 애플리케이션에서 쓰기 좋은 유연한 오픈 소스 인증 솔루션입니다. Google, GitHub, Kakao 같은 여러 소셜 로그인 공급자(provider)를 쉽게 붙일 수 있고, 자격 증명(Credentials) 방식도 연결할 수 있습니다. 다만 비밀번호의 검증, 저장, 가입 정책은 애플리케이션이 직접 구현해야 합니다. 세션 관리, JWT(JSON Web Token), OAuth 같은 복잡한 인증 로직을 추상화해 인증 구현에 드는 노력을 줄여 줍니다.
App Router에서는 src/auth.ts가 내보낸 handlers를 인증 라우트에 연결하고, 서버 컴포넌트와 Route Handler에서 auth()로 세션을 확인합니다. Route Handler와 서버 액션은 각 작업의 권한도 따로 확인해야 하며, 인증 라우트를 연결했다고 해서 모든 작업이 보호되지는 않습니다. v5의 auth() API를 쓰려면 npm install next-auth@beta로 설치합니다. 자세한 내용은 Auth.js 문서에서 볼 수 있습니다.
상태 관리(State Management)
클라이언트 컴포넌트에서 복잡한 전역 상태를 효율적으로 관리하기 위한 라이브러리들입니다. 아래의 서버 상태 도구는 서버에서 받은 데이터의 재조회와 동기화도 맡으므로, UI에서 직접 만든 상태와 구분해서 봅니다.
같은 화면에서도 값마다 성격이 다릅니다. 재고 목록에서 설명의 열림 여부는 그 컴포넌트의 로컬 상태로 시작할 수 있습니다. 여러 화면이 공유하는 정렬 조건은 새로고침과 공유를 위해 URL에 남길지, 클라이언트 스토어에서만 유지할지 먼저 정합니다. 서버가 보내는 현재 재고 수량은 여러 사용자가 바꿀 수 있으므로 서버 데이터입니다. 이를 조회한 캐시는 DB 원본이나 구매 가능 여부의 최종 판정을 대신하지 않습니다. 같은 재고 데이터를 SWR과 React Query 양쪽에서 따로 캐시하면 한쪽만 갱신될 수도 있으므로, 둘을 함께 도입하기 전에 어느 쪽이 조회와 갱신을 맡을지 정합니다.
Zustand
Zustand는 작고 빠르며 확장하기 쉬운 상태 관리 라이브러리입니다. 비교적 적은 코드로 스토어를 정의하고 업데이트하며, 미들웨어와 개발자 도구 연동도 지원합니다. React Hook을 쓰는 UI는 클라이언트 컴포넌트 안에 둡니다. SSR에서는 스토어를 요청마다 만들고 서버와 클라이언트의 초기 데이터를 맞추며, 서버 컴포넌트에서 공유 전역 스토어를 읽거나 고치지 않습니다. Zustand의 vanilla 스토어 API 자체가 브라우저 전용인 것은 아닙니다. npm install zustand로 설치하며, 문서는 Zustand 문서에 있습니다.
Jotai
Jotai는 원자적(atomic) 상태 관리를 지향하는 작은 라이브러리입니다. React의 useState와 비슷한 atom 개념으로 상태를 정의하며, atom 사이의 의존 관계로 필요한 구독 업데이트를 줄일 수 있습니다. 다만 렌더 횟수의 최솟값을 보장하지는 않습니다. Hook을 쓰는 UI는 클라이언트 컴포넌트에 두고, SSR에서 요청 사이의 상태가 섞이지 않도록 알맞은 Provider로 스토어 범위를 정하며, 서버와 클라이언트의 초기값을 맞춥니다. npm install jotai로 설치하며, 문서는 Jotai 문서에 있습니다.
TanStack Query(React Query)
TanStack Query는 서버 상태(server state) 관리에 특화된 라이브러리입니다. 데이터 페칭, 캐싱, 동기화, 업데이트처럼 비동기 데이터와 관련된 복잡한 작업을 선언적으로 처리하고, 로딩, 오류, 성공 상태를 자동으로 관리해 반복되는 코드를 줄여 줍니다. Next.js에서는 서버에서 데이터를 미리 가져와 클라이언트에서 하이드레이션하는 방식으로 사용자 경험을 개선할 수 있습니다. App Router에서는 서버 컴포넌트에서 초기 데이터를 가져오고, 클라이언트 컴포넌트에서 TanStack Query로 추가 데이터 관리와 UI 업데이트를 처리하는 방식이 일반적입니다. npm install @tanstack/react-query로 설치하며, 문서는 TanStack Query 문서에 있습니다.
SWR
SWR은 Vercel이 만든 데이터 페칭 라이브러리로, TanStack Query처럼 서버 상태 관리에 씁니다. stale-while-revalidate 전략에 따라 캐시된 데이터를 바로 반환하고, 백그라운드에서 최신 데이터를 다시 가져와 업데이트합니다. TanStack Query와 마찬가지로 Next.js의 데이터 페칭 방식과 함께 쓰기 좋습니다. npm install swr로 설치하며, 문서는 SWR 문서에 있습니다.
UI 컴포넌트 및 스타일링
보기 좋고 반응성 있는 UI를 빠르게 만드는 데 도움을 주는 라이브러리들입니다. 이 범주는 스타일과 동작을 어디까지 대신해 주는지에 따라 성격이 나뉩니다. Tailwind CSS는 클래스로 스타일을 직접 조합하는 CSS 도구이고, Chakra UI는 스타일과 동작을 모두 갖춘 컴포넌트를, Radix UI는 스타일 없이 동작과 접근성만 갖춘 프리미티브를 제공합니다. 디자인을 직접 정할 여지가 클수록 손이 더 가므로, 팀의 디자인 시스템이 있는지부터 확인하고 고릅니다.
Tailwind CSS
Tailwind CSS는 유틸리티 우선(utility-first) CSS 프레임워크입니다. 작은 단위의 유틸리티 클래스를 조합해 화면을 만들고, CSS의 @theme에서 디자인 토큰을 선언합니다. Tailwind CSS 4에서는 @tailwindcss/postcss를 PostCSS 플러그인으로 등록하고 전역 CSS에 @import "tailwindcss";를 적습니다. 일반적인 프로젝트에는 별도의 content 배열이나 Purge 설정이 필요하지 않습니다. npm install -D tailwindcss @tailwindcss/postcss postcss로 설치하며, 문서는 Tailwind CSS 문서에 있습니다.
Chakra UI
Chakra UI는 접근성(accessibility)을 중시하는 React 컴포넌트 라이브러리로, 버튼, 폼, 다이얼로그 같은 컴포넌트와 일관된 스타일 속성, 레시피를 제공합니다. Chakra UI 3의 CLI snippet이 만드는 Provider를 App Router의 layout.tsx에 둡니다. 이 Provider는 Chakra 설정과 next-themes 기반의 색상 모드를 함께 묶습니다. Turbopack에서 Emotion의 하이드레이션 불일치가 보이면 Chakra의 공식 Next.js 가이드에 따라 개발과 빌드의 번들러 조건을 맞춰 확인합니다. npm install @chakra-ui/react @emotion/react로 설치한 뒤 npx @chakra-ui/cli snippet add를 실행하며, 문서는 Chakra UI 문서에 있습니다.
Radix UI
Radix UI는 특정 CSS 프레임워크나 디자인 시스템에 묶이지 않는 저수준(low-level) UI 프리미티브(primitive) 라이브러리입니다. 스타일 없이 컴포넌트의 동작과 접근성에 집중하는 headless 방식이므로, 스타일은 개발자가 직접 정의합니다. CSS Modules, Tailwind CSS, styled-components 같은 어떤 스타일링 방식과도 함께 쓸 수 있습니다. 프리미티브가 키보드 이동이나 포커스 관리 같은 동작을 돕더라도, 아이콘만 있는 버튼의 접근 가능한 이름과 올바른 컴포넌트 조합은 애플리케이션이 제공해야 합니다. 완성한 화면에서 키보드로 열고 닫을 때 포커스가 제자리로 돌아오는지도 확인합니다. npm install @radix-ui/react-dialog처럼 필요한 컴포넌트 패키지를 하나씩 설치하거나, 프리미티브를 묶은 radix-ui 패키지를 설치하며, 문서는 Radix UI 문서에 있습니다.
폼 유효성 검사(Form Validation)
사용자 입력 폼의 유효성을 검사하고 관리하는 데 쓰는 라이브러리들입니다. 아래 두 라이브러리는 맡는 일이 다릅니다. React Hook Form은 입력값, 오류 메시지, 제출 상태 같은 폼의 상태를 관리하고, Zod는 값이 정해진 형태인지 검사하는 스키마를 정의합니다. 그래서 둘을 함께 쓰는 경우가 많으며, 같은 Zod 스키마를 서버 액션의 입력 검사에도 다시 쓸 수 있습니다.
React Hook Form
React Hook Form은 다시 렌더링이 적고, Zod나 Yup 같은 유효성 검사 도구와 쉽게 통합되며, 적은 코드로 복잡한 폼을 처리하는 폼 관리 라이브러리입니다. useForm 훅으로 폼 상태와 유효성 검사 로직을 관리합니다. 주로 클라이언트 컴포넌트에서 폼을 정의할 때 쓰며, 서버 액션과 함께 쓸 때는 클라이언트에서 1차 유효성 검사를 하고 서버에서 2차 검사를 해서 데이터 무결성을 지킵니다. npm install react-hook-form으로 설치하며, 문서는 React Hook Form 문서에 있습니다.
Zod
Zod는 TypeScript 우선의 스키마 유효성 검사 라이브러리입니다. 런타임 검증 스키마를 정의하고, 그 스키마에서 정적 TypeScript 타입을 추론합니다. TypeScript 타입 선언만으로 런타임 검사가 생기지는 않으므로, React Hook Form이나 서버 액션과 함께 써서 클라이언트와 서버 양쪽에서 데이터의 형태와 유효성을 확인합니다. 서버 액션의 입력값 검사에 Zod 스키마를 쓰고, 클라이언트 컴포넌트에서는 @hookform/resolvers의 zodResolver로 React Hook Form과 연결할 수 있습니다. 스키마 검증이 작업 권한 검사를 대신하지는 않습니다. npm install zod로 설치하고, React Hook Form과 연결한다면 @hookform/resolvers도 설치하며, 문서는 Zod 문서에 있습니다.
입력 형태의 검사와 작업 권한은 이어서 확인합니다. FormData.get()은 숫자 입력란의 값도 보통 문자열로 돌려줍니다. z.number()에 문자열 "3"을 그대로 넣으면 숫자 검증에 실패합니다. 다음은 수량을 1 이상 99 이하의 정수로 받는 예입니다. 빈 문자열과 파일 입력을 먼저 빼고, 의도적으로 숫자로 바꾼 뒤 런타임 검사를 합니다.
import { z } from 'zod';
const Quantity = z.number().int().min(1).max(99);
export function parseQuantity(formData: FormData): number | null {
const raw = formData.get('quantity');
if (typeof raw !== 'string' || raw.trim() === '') return null;
const result = Quantity.safeParse(Number(raw));
return result.success ? result.data : null;
}"3"은 3이 되지만 "abc", "3.5", "100"은 이 조건을 만족하지 않습니다. 빈 문자열은 Number("")가 0이 되기 전에 거절합니다. 이 예는 숫자로 바꾼 결과를 검사하므로, 입력 표기까지 십진 숫자로만 제한하려면 별도의 형식 조건도 정해야 합니다.
수량 3을 얻었어도 사용자 2에게 사용자 1의 장바구니 항목을 바꿀 권한이 생기지는 않습니다. 서버는 검증한 세션에서 사용자 ID를 얻고, DB에 저장된 항목의 소유자와 비교한 뒤 변경합니다. 폼이 보낸 ownerId를 소유권을 판단하는 값으로 믿지 않습니다. 클라이언트 검사는 빠른 피드백을 주고, 서버 검사는 직접 보낸 요청에도 적용됩니다.
데이터베이스 ORM/ODM
데이터베이스와 애플리케이션 사이의 상호작용을 돕는 라이브러리들입니다. MongoDB 같은 문서 데이터베이스에는 ODM(Object Document Mapper)을, PostgreSQL 같은 관계형 데이터베이스에는 ORM(Object-Relational Mapper)을 씁니다. 두 종류 모두 데이터의 모양을 코드로 정의하고 쿼리를 함수로 제공하지만, 연결 정보와 쿼리를 서버에서만 실행해야 한다는 점은 같습니다. 그래서 아래 두 라이브러리 모두 서버 전용 데이터 계층에 둡니다.
Mongoose
Mongoose는 Node.js 환경에서 MongoDB를 다루는 ODM 라이브러리입니다. 스키마를 바탕으로 데이터를 정의하고, 데이터 유효성 검사, 쿼리 빌더, 미들웨어 같은 기능을 제공해 MongoDB를 편리하게 쓰게 합니다. Route Handler, 서버 컴포넌트, 서버 액션에서 MongoDB를 쓸 때는 연결 로직을 lib/db.ts 같은 서버 전용 모듈로 분리합니다. Mongoose는 Node.js 런타임을 쓰며, Next.js의 Edge Runtime에서 MongoDB에 직접 연결하는 방식은 지원하지 않습니다. npm install mongoose로 설치하며, 문서는 Mongoose 문서에 있습니다.
Prisma
Prisma는 Node.js와 TypeScript를 위한 ORM(객체 관계형 매핑)입니다. 데이터베이스 스키마를 정의하면 이를 바탕으로 타입 안전한 쿼리 클라이언트를 생성하며, PostgreSQL, MySQL, SQLite 같은 여러 관계형 데이터베이스를 지원합니다. Route Handler, 서버 컴포넌트, 서버 액션의 데이터 계층에서 쓰고, 생성된 Prisma Client의 타입을 서버 데이터 계층에서 활용합니다. 고른 Prisma 버전, DB, 런타임에 맞는 드라이버나 어댑터와 생성 설정도 필요합니다. 2026년 11월 현재 버전을 붙이지 않은 npm install -D prisma는 아직 릴리스 후보(RC)인 Prisma 8의 CLI를 설치하는데, 이 CLI는 schema.prisma를 읽지 않고 generate 명령도 없습니다. 반면 @prisma/client는 Prisma 7에 속한 패키지라 7.10.1에 머물러 있으므로, 위 설명처럼 스키마를 정의하고 클라이언트를 생성하려면 npm install -D prisma@7과 npm install @prisma/client@7로 두 패키지의 주 버전을 맞춰 설치하고 npx prisma@7 init으로 초기 설정을 만듭니다. 문서는 Prisma 7 문서에 있습니다.
기타 유용한 라이브러리
그 밖에 자주 쓰는 작은 라이브러리들은 다음과 같습니다.
use-debounce: 검색어나 텍스트 입력 같은 사용자 입력의 이벤트를 디바운스(debounce)해 과도한 함수 호출이나 API 요청을 막습니다. 검색 필터 기능을 만들 때 유용하며,npm install use-debounce로 설치합니다.react-icons: Font Awesome, Material Design Icons 같은 인기 아이콘 모음의 아이콘을 React 컴포넌트로 제공합니다.npm install react-icons로 설치합니다.date-fns,dayjs,moment: 날짜와 시간의 포매팅과 계산을 돕는 유틸리티 라이브러리입니다. 간단한 표시라면 표준IntlAPI부터 확인하고, 추가 연산이 필요하면 필요한 함수와 번들 영향을 비교합니다. Moment는 유지보수 모드에 들어간 기존 프로젝트이므로, 새로 도입하기 전에 유지보수 정책을 먼저 확인합니다.
검색에 디바운스를 쓸 때는 요청이 끝나는 순서도 다뤄야 합니다. 디바운스는 입력이 잦을 때 아직 실행하지 않은 호출을 늦추거나 취소하는 데 도움이 되지만, 이미 시작한 요청을 취소하거나 응답 순서를 보장하지는 않습니다. 검색 A의 요청이 시작된 뒤 검색 B의 요청이 시작되고 B의 응답이 먼저 왔다면, 늦게 온 A의 응답이 화면을 덮지 않도록 요청 번호를 비교하거나, 요청 취소와 오래된 응답 무시 정책을 함께 구현합니다. 디바운스의 대기 호출 취소와 네트워크 요청 취소는 서로 다른 동작입니다.
Next.js 생태계의 라이브러리는 프로젝트의 요구사항, 유지보수성, 번들 영향, 팀의 숙련도를 기준으로 고릅니다. 새 기능을 더할 때는 라이브러리가 주는 추상화가 실제 복잡도를 줄이는지도 함께 확인합니다. 도서 상세에서 접힌 설명의 열림 상태만 저장한다면 먼저 컴포넌트의 useState로 충분한지 봅니다. 여러 화면이 함께 읽는 UI 상태, 반복해서 조회할 서버 데이터, 폼 입력의 검증은 서로 다른 요구이므로, 라이브러리 이름을 먼저 고르기보다 저장할 값과 실행 위치를 적어 보고 선택합니다.
정리
- 라이브러리는 기본 기능과 기존 의존성으로 해결하기 어려운 요구가 있을 때 도입하며, 저장할 값과 실행 위치를 먼저 적어 봅니다.
- Auth.js는 로그인과 세션을 맡지만, 각 작업의 권한 검사는 Route Handler와 서버 액션에서 따로 합니다.
- Zustand와 Jotai는 클라이언트 UI 상태를, TanStack Query와 SWR은 서버 데이터의 캐시와 재조회를 다루며, SSR에서는 요청마다 상태 범위를 나눕니다.
- Zod 같은 스키마 검증은 입력의 형태를 확인할 뿐, 소유권과 권한 검사를 대신하지 않습니다.
- 디바운스는 아직 실행하지 않은 호출만 다루므로, 이미 시작한 요청의 응답 순서는 따로 처리합니다.
연습 문제
1. 형식 검사와 소유권 검사
장바구니 수량 폼이 있고, 서버에서 사용자의 항목만 바꿔야 한다고 합시다. 수량의 형식 검사를 통과하면 항목의 소유권 검사도 끝난 것일까요? 로그인, 입력 검증, 서버 데이터 재조회, UI 열림 상태를 모두 하나의 전역 상태 라이브러리로 해결해야 할까요?
해설 보기
수량이 올바른 숫자인지는 입력 검증의 질문이고, 그 사용자가 그 항목을 바꿀 수 있는지는 서버 권한 검사의 질문입니다. 로그인 세션을 확인한 뒤에도 항목의 소유자를 검사합니다. 서버에서 받은 장바구니를 재조회하고 동기화하는 일과 폼의 열림 상태를 관리하는 일도 구분합니다. 작은 UI 상태에는 로컬 상태로 충분할 수 있고, 서버 데이터의 캐시에는 그 요구에 맞는 도구를 검토합니다.
채택한 패키지의 버전, 지원 런타임, 기본 설정을 기록하고, 그 라이브러리를 제거했을 때 사라지는 구체적인 복잡도를 설명해 봅니다. 설치 명령을 실행했다는 사실만으로 세션 정책, 업무 검증, 서버 권한까지 구현된 것은 아닙니다.
2. 지역 상태와 서버 데이터
한 재고 목록에서 설명을 열고 닫는 값과 서버가 보내는 재고 수량을 관리합니다. 열림 여부는 다른 화면과 공유하지 않습니다. 두 값을 처음부터 모두 전역 UI 스토어에 넣어야 할까요? 구매 가능 여부는 어디에서 최종 확인할까요?
해설 보기
아닙니다. 열림 여부는 해당 컴포넌트의 로컬 상태로 시작할 수 있습니다. 재고 수량은 서버 데이터이므로 재조회와 캐시 갱신의 요구를 검토합니다. 표시용 캐시가 있어도 구매할 때의 권한과 재고 조건은 서버가 확인해야 합니다. 두 값은 저장 위치뿐 아니라 어느 쪽이 최신값을 결정하는지도 다릅니다.
3. 폼이 보낸 소유자 ID
검증된 세션의 사용자 ID는 2이고, DB에 저장된 장바구니 항목의 소유자는 1입니다. 폼은 ownerId=2와 수량 "3"을 보냈습니다. 수량 검사가 성공하면 항목을 바꿔도 될까요?
해설 보기
바꾸면 안 됩니다. 수량의 형태와 범위는 유효해도 사용자 2는 항목의 소유자 1과 다릅니다. 폼의 ownerId는 사용자가 바꿀 수 있는 입력이므로 소유권을 증명하지 못합니다. 검증된 세션의 사용자와 DB의 소유자를 비교한 결과로 요청을 거절합니다.
4. parseQuantity의 반환값
위 parseQuantity에 수량 문자열 "3", "", "abc", "3.5", "100"을 각각 넣으면 어떤 값이 반환될까요? TypeScript에서 입력을 number라고 선언하는 것만으로 같은 검사가 생길까요?
해설 보기
차례대로 3, null, null, null, null입니다. 빈 문자열은 변환 전에 거절하고, "abc"는 숫자로 바꿔도 유효한 수가 아니며, 3.5는 정수가 아니고 100은 상한을 넘습니다. TypeScript의 타입 선언은 실행 중에 들어온 폼 값의 범위나 형태를 자동으로 검사하지 않습니다.
5. 디바운스와 늦게 온 응답
검색 A와 B의 요청은 이미 시작되었습니다. B의 응답을 표시한 뒤 A의 응답이 도착했습니다. 디바운스의 대기 호출을 취소하면 A가 화면을 덮는 문제까지 해결될까요?
해설 보기
해결되지 않습니다. A는 대기 중인 호출이 아니라 이미 실행 중인 요청입니다. 현재 검색의 요청 번호와 응답의 요청 번호를 비교해 오래된 A를 무시하는 등의 정책이 필요합니다. 요청 취소를 쓰더라도 완료 시점이 겹칠 수 있으므로, 오래된 응답을 적용하지 않는 조건은 그대로 유지합니다.
Next.js와 함께 쓰는 인증과 폼 처리의 기본 흐름은 Next.js의 인증 안내와 Next.js의 폼 안내에서 확인할 수 있습니다.
다음 글 커뮤니티 리소스 및 포럼에서는 Next.js를 공부하며 도움을 구할 수 있는 커뮤니티와 포럼을 알아봅니다.