학습 목표·선수 지식: FilterBar를 Suspense로 감싸 정적 빌드 오류를 해결합니다. 선수 지식: Client Component·쿼리스트링.
이 글에서 정리하는 내용
useSearchParams 빌드 오류는 렌더링 경계와 Suspense 배치를 먼저 확인해야 합니다. metadata·canonical까지 포함한 전체 검색 최적화 흐름은 Next.js SEO 가이드에서 이어서 볼 수 있습니다.
실습 경로: 현재 ZIP → 전체 코드. 먼저 설명에서 지정한 파일만 읽고, 설정·테스트 파일은 필요한 때 확인하세요.
useSearchParams 오류가 빌드에서 터지는 이유

App Router의 정적 페이지에서 클라이언트 컴포넌트가 useSearchParams를 사용하면 production build에서 Suspense 경계가 필요할 수 있습니다. 개발 서버에서는 on-demand로 렌더링되어 괜찮아 보이다가, next build에서만 Missing Suspense boundary with useSearchParams 오류가 드러나는 패턴이 흔합니다.
먼저 useSearchParams가 어느 컴포넌트에서 호출되는지 찾습니다. 직접 작성한 검색 바뿐 아니라 progress bar, analytics provider, layout 하위 client component가 내부에서 호출하는 경우도 있습니다. 배포 흐름까지 같이 확인하려면 프론트엔드 배포 로드맵도 함께 보면 좋습니다.
App Router에서 클라이언트 훅을 쓰는 위치
useSearchParams는 Client Component에서 현재 URL의 query string을 읽는 hook입니다. Server Component 페이지에서 검색 조건으로 데이터를 가져와야 한다면 page의 searchParams prop을 먼저 쓰고, 상호작용이 필요한 작은 영역만 client component로 분리하는 편이 단순합니다.
검색 UI처럼 URL query를 읽기만 하는 부분은 client component로 작게 분리하고, 그 컴포넌트를 가장 가까운 Suspense로 감싸는 기준이 좋습니다. 페이지 전체를 무작정 client component로 바꾸면 정적 렌더링과 서버 데이터 조회의 장점을 잃기 쉽습니다.
아래 수정 후 page.tsx에서 FilterBar를 부모 Suspense가 감쌉니다. 경계 안에서 훅을 호출하고 fallback 범위는 검색 UI로 한정합니다.
만약 route 자체를 요청 시점에 동적으로 렌더링해야 한다면 Server Component에서 connection()을 사용하는 방식도 검토할 수 있습니다. 다만 단순 검색 바 문제라면 먼저 작은 client component와 Suspense 경계로 해결되는지 확인하는 것이 변경 범위가 작습니다.
Suspense로 감싸야 하는 컴포넌트 기준
Suspense로 감싸야 하는 대상은 useSearchParams를 직접 호출하거나 그 hook을 내부에서 쓰는 client component입니다. 검색 바, 필터 패널, 페이지네이션 컴포넌트처럼 query string을 읽는 작은 UI를 분리하면 fallback 범위도 작게 유지됩니다.
오류가 dev에서는 보이지 않고 build에서만 보이면 정상적인 단서입니다. 수정 후에는 npm run build로 다시 확인하고, 캐시나 데이터 갱신 문제와 섞이면 Next.js fetch 캐시 문제 해결처럼 렌더링 기준을 따로 분리해 봅니다.
검색·필터 UI에서 안전한 구조

검색·필터 UI에서는 page가 searchParams prop으로 초기 조건을 읽고, client component는 사용자가 값을 바꿀 때 URL을 갱신하는 역할로 나누면 구조가 단순합니다. 이렇게 하면 서버 데이터 조회와 클라이언트 상호작용의 책임이 섞이지 않습니다.
URL query를 읽는 컴포넌트가 여러 곳에 흩어져 있다면 먼저 하나의 FilterBar 같은 작은 단위로 모읍니다. layout 전체나 provider 전체가 useSearchParams에 의존하면 Suspense 범위가 커지고, 빌드 오류 위치도 추적하기 어려워집니다.
Vercel 배포 전 체크리스트
로컬에서 수정한 뒤에는 npm run build를 다시 실행해 같은 오류가 사라졌는지 확인합니다. 그다음 Vercel에서 동일한 Node 버전과 환경변수로 빌드되는지 보고, 검색·필터 URL을 직접 새로고침해도 페이지가 정상적으로 렌더링되는지 확인합니다.
특히 useSearchParams 호출 위치를 옮겼다면 변경 전후의 컴포넌트 경계를 확인하는 것이 중요합니다. FilterBar처럼 query string을 읽는 작은 Client Component만 Suspense 안에 두고, 페이지 전체를 Client Component로 바꾸지 않았는지 점검합니다. 이렇게 하면 빌드 오류를 해결하면서도 서버 렌더링 범위를 불필요하게 넓히지 않을 수 있습니다.
수정이 끝나면 개발 서버가 아니라 production build 결과를 기준으로 판단합니다. 개발 환경에서만 정상으로 보이는 경우가 있기 때문에 next build와 실제 배포 URL에서 검색 조건 변경, 직접 진입, 새로고침까지 확인해야 해결 여부를 확실히 판단할 수 있습니다.
FilterBar 전체 구현과 확인 순서
page는 searchParams를 읽지 않고 정적 제목을 유지합니다. FilterBar만 현재 query를 읽고 form 제출 시 q를 바꾸되 다른 query 키는 보존합니다. 이것은 서버 page가 searchParams로 데이터를 조회하는 요청별 렌더링 대안과 구분되는 실습입니다. 아래 프로젝트의 두 파일과 설정을 그대로 사용하세요.
/?q=router&page=2에 직접 들어가 현재 검색어를 확인합니다. 검색을 바꾸면 page=2가 보존되는지, 빈 검색을 제출하면 q만 사라지는지 확인합니다. 가장 가까운 부모 Suspense를 제거한 별도 시작 상태에서는 production build의 Missing Suspense boundary 오류를 재현합니다.
현재 실습 ZIP
전체 실행 파일
ZIP의 starter는 의도적으로 빌드에 실패하는 수정 전 프로젝트, complete는 수정 후 프로젝트입니다. 각 폴더에서 npm install 후 npm run build로 비교합니다. 수정 후 실행은 npm run start, 개발은 npm run dev, 타입 검사는 npm run typecheck입니다. Node.js20.9 이상(검증24.19.0), Next16.3.5·React19.3.0·TS7.0.2입니다.
README.md
# 실습 4455
Node.js 20.9 이상 (검증: 24.19.0). 이 폴더에서 npm install, npm run build, npm run start. 개발은 npm run dev. 타입 검사는 npm run typecheck.
Next.js 16.3.5 / React 19.3.0 / TypeScript 7.0.2. 외부 DB·인증 서비스는 제공하지 않습니다.
검증: 로컬 production build와 HTTP 검사. 브라우저 조작과 실제 배포는 별도 확인합니다.
app/FilterBar.tsx
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export default function FilterBar() {
const query = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const q = query.get("q") ?? "";
return (
<section>
<form
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const next = new URLSearchParams(query.toString());
const value = String(data.get("q") ?? "").trim();
if (value) next.set("q", value);
else next.delete("q");
router.replace(pathname + (next.size ? "?" + next.toString() : ""));
}}
>
<label>
검색어 <input key={q} name="q" defaultValue={q} />
</label>
<button>검색</button>
</form>
<p>현재 검색어: {q || "전체"}</p>
</section>
);
}
app/layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}
app/page.tsx
import FilterBar from "./FilterBar";
export default function Page() {
return <FilterBar />;
}
next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package.json
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start",
"typecheck": "next typegen && tsc --noEmit"
},
"engines": {
"node": ">=20.9.0"
},
"dependencies": {
"next": "16.3.5",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"typescript": "7.0.2",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@types/node": "22.20.2"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "esnext"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"plugins": [
{
"name": "next"
}
],
"allowJs": true,
"incremental": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
app/page.tsx를 교체합니다
FilterBar를 그대로 두고 app/page.tsx에서 Suspense를 import합니다. FilterBar 바깥에 fallback이 있는 Suspense 경계를 추가하고 정적 h1을 경계 밖에 둡니다. 훅을 호출한 컴포넌트 내부가 아니라 그 부모가 감싸야 합니다.
다른 파일은 변경하지 않습니다. starter에서 npm run build로 실패를 확인하고 app/page.tsx만 수정 후 complete와 대조합니다. 다시 npm run build가 성공해야 합니다. 아래 코드는 diff가 아닌 전체 파일이므로 + 또는 – 기호를 붙여 복사하지 않습니다.
실제 검증: 시작 상태 build exit1 → 수정 후 exit0. 수정 후 HTTP200 확인. 직접 query URL과 검색 form 동작는 별도 확인합니다.
README.md
# 실습 4455
Node.js 20.9 이상 (검증: 24.19.0). 이 폴더에서 npm install, npm run build, npm run start. 개발은 npm run dev. 타입 검사는 npm run typecheck.
Next.js 16.3.5 / React 19.3.0 / TypeScript 7.0.2. 외부 DB·인증 서비스는 제공하지 않습니다.
검증: 로컬 production build와 HTTP 검사. 브라우저 조작과 실제 배포는 별도 확인합니다.
app/FilterBar.tsx
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export default function FilterBar() {
const query = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const q = query.get("q") ?? "";
return (
<section>
<form
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const next = new URLSearchParams(query.toString());
const value = String(data.get("q") ?? "").trim();
if (value) next.set("q", value);
else next.delete("q");
router.replace(pathname + (next.size ? "?" + next.toString() : ""));
}}
>
<label>
검색어 <input key={q} name="q" defaultValue={q} />
</label>
<button>검색</button>
</form>
<p>현재 검색어: {q || "전체"}</p>
</section>
);
}
app/layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}
app/page.tsx
import { Suspense } from "react";
import FilterBar from "./FilterBar";
export default function Page() {
return (
<main>
<h1>검색 실습</h1>
<Suspense fallback={<p>검색 조건을 불러오는 중</p>}>
<FilterBar />
</Suspense>
</main>
);
}
next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package.json
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start",
"typecheck": "next typegen && tsc --noEmit"
},
"engines": {
"node": ">=20.9.0"
},
"dependencies": {
"next": "16.3.5",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"typescript": "7.0.2",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@types/node": "22.20.2"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "esnext"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"plugins": [
{
"name": "next"
}
],
"allowJs": true,
"incremental": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
실제 검증: next build: Suspense 없는 시작본 exit 1 → 수정본 exit 0; HTTP / 및 query URL 검사. 브라우저 form 조작 미검증. 코드 뷰어와 ZIP의 모든 파일을 바이트/복사 텍스트로 대조했습니다.
공식 자료 재확인: 2026-09-12. 이 글의 실행하지 않은 브라우저/배포 점검은 독자 확인 과제로 구분합니다.
공식 기준과 확인 범위
확인일: 2026-09-12. API 설명은 Next.js 16 App Router 기준이며 과거 릴리스 글은 본문의 태그 범위를 유지합니다. 부분 API 코드는 기존 프로젝트에 통합하는 예시입니다.
공식 문서
이 글이 도움이 되었나요?
Next.js 학습 순서
필수 13개 · 전체 27개
읽음 기록 관리
전체 과정 목차 (27개)
- 필수 길잡이 · Next.js App Router 학습 순서: 설치부터 배포까지
- 필수 학습 · Next.js package.json: scripts dependencies 이해
- 필수 학습 · Next.js App Router로 만드는 첫 프로젝트 완성 실습 가이드
- 필수 학습 · Next.js에서 .next 폴더는 어떤 역할을 할까?
- 필수 학습 · Next.js 동적 라우트 완전 정리: [slug], params, catch-all
- 선택 참고 · Next.js params should be awaited 해결: App Router 기준
- 선택 참고 · Next.js window is not defined 오류 해결: 브라우저 API를 안전하게 쓰기
- 선택 참고 · Next.js hydration failed 오류 해결: 원인과 해결 방법
- 필수 학습 · Axios 사용법: React Next.js에서 API 요청 구조 잡는 법
- 선택 참고 · Next.js Route Handler 405 오류 해결: GET/POST 파일 위치와 메서드 설정 확인
- 필수 학습 · Next.js Server Actions + React Hook Form 검증 기준
- 선택 참고 · Next.js useSearchParams Suspense 오류 해결: 빌드 실패 기준 현재 글
- 선택 참고 · Next.js fetch 캐시 문제 해결: 데이터가 바뀌었는데 화면이 그대로일 때
- 선택 참고 · Next.js Dynamic server usage 오류 해결: cookies headers 기준
- 선택 참고 · Next.js 환경변수 적용 오류 해결: .env.local을 바꿨는데 값이 안 바뀔 때
- 필수 학습 · Next.js Metadata API 완전 정리: 정적 metadata와 generateMetadata
- 선택 참고 · Next.js metadata가 적용되지 않을 때 확인할 7가지
- 선택 참고 · Next.js Image remotePatterns 오류 해결: 외부 이미지 도메인 허용하기
- 필수 학습 · Next.js redirects 설정: next.config.js에서 URL 이동 처리
- 필수 학습 · Next.js SEO 체크리스트: metadata·초기 HTML·OG 이미지 점검
- 선택 참고 · Next.js SEO 완전 가이드: App Router metadata부터 배포 확인까지
- 필수 학습 · Next.js 16 성능 최적화 체크리스트: 번들·이미지·캐시·배포
- 필수 학습 · Next.js 렌더링 성능 최적화: React 화면이 느릴 때 기준
- 필수 학습 · Next.js 16 Proxy 마이그레이션: Node.js Runtime·matcher 검증
- 선택 참고 · Next.js 보안 패치 기준: v16.2.5 영향 범위 점검
- 선택 참고 · Next.js SEO SSR 적용법: 검색 노출과 렌더링 구조 잡기
- 시점·기록 · Next.js 16.3.0-canary.106의 useCache deprecation 경고와 hybrid not-found 수정 이해하기
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.