1. useSearchParams를 클라이언트 컴포넌트에만 사용하라
vercel build error가 남
⨯ useSearchParams() should be wrapped in a suspense boundary at page "/games". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
at g (/vercel/path0/.next/server/chunks/956.js:7:58171)
at l (/vercel/path0/.next/server/chunks/956.js:5:44390)
분명 useSearchParams()를 클라이언트컴포넌트내부에서만 사용하는데?
부모 컴포넌트가 서버 컴포넌트더라도, useSearchParams()를 사용하는 자식 컴포넌트가 클라이언트 컴포넌트이면 원칙적으로는 문제없지만
다음 조건 중 하나라도 충족하지 않으면 에러가 날 수 있다.
정상 작동 조건 정리
useSearchParams()를 사용하는 컴포넌트 파일 상단에'use client'선언이 있어야 함- 그 컴포넌트는 직접 렌더링되거나, 동적으로 로딩되어야 함
- 해당 클라이언트 컴포넌트의 렌더링이 SSR 중 실행되지 않아야 함
- App Router 구조에서만 동작하며,
pages/디렉토리 기반에서는 사용 불가
내 경우, 해당 클라이언트 컴포넌트의 렌더링이 SSR 중 실행되는게 원인이다.
클라이언트 컴포넌트를 서버 컴포넌트 안에서 직접 사용하고, useSearchParams()가 초기에 실행되는 경우
서버 사이드 렌더링 중 useSearchParams()가 실행되어 Next.js가 에러를 발생시킨다.
해결 방법
lazy loading으로 SSR 끄기
- Next.js 전용 lazy-loading 유틸
- 내부적으로 dynamic import() + React.lazy() + hydration-safe 처리까지 함
import dynamic from 'next/dynamic';
const MyComponent = dynamic(() => import('./MyComponent'), { ssr: false });
export default function Page() {
return (
<div>
<h1>보드게임</h1>
<MyComponent /> {/* 이러면 CSR로만 렌더링됨 */}
</div>
);
}
또는,
로딩 상태까지 처리한다면
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
const MyComponent = dynamic(() => import('./MyComponent'), {
suspense: true,
});
export default function Page() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<MyComponent />
</Suspense>
);
}
- fallback: MyComponent가 로딩 중일 때 보여줄 UI
- 해당 컴포넌트가 CSR + 비동기 로딩될 때 UX 향상을 위해 로딩 UI를 제공하고 싶을 때 유용하다.
ssr: false를 쓰면 CSR전용 컴포넌트로 만들어 동적 import하는데, suspense와 fallback을 못씀suspense: true는React.lazy()처럼 작동한다. SSR도 가능하다.(기본 true)
그런데 useSearchParams()때문에 SSR을 회피하는 목적이라면 ssr:false만으로 충분하다.... !
클라이언트 컴포넌트에서만 useSearchParams() 사용되도록 depth 분리
// page.tsx
import GameListWrapper from './GameListWrapper';
export default function Page() {
return (
<div>
<GameListWrapper />
</div>
);
}
// GameListWrapper.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function GameListWrapper() {
const searchParams = useSearchParams();
...
}
GameListWrapper는 use client 선언된 독립된 컴포넌트이며, 직접 서버에서 프리렌더링하지 않기 때문에 에러가 나지 않는다.
2. 404페이지와 에러페이지
error.tsx와 not-found.tsx(404페이지)는 Next.js(App Router 기준)에서 전혀 다른 목적의 에러 처리 파일이다.
404페이지(not-found)
404페이지는 notFound() 함수 호출 시 자동으로 렌더링하거나
존재하지 않는 게시물, 잘못된 slug 등 리소스를 찾을 수 없는 경우에 사용한다.
not-found페이지 작성법
// app/not-found.tsx
export default function NotFoundPage() {
return (
<div>
<h1>404 - 페이지를 찾을 수 없습니다</h1>
<p>요청하신 리소스가 존재하지 않아요.</p>
</div>
);
}
notFound() 사용법
특정 데이터 없을 때 404 유도하는 방법이다.
import { notFound } from 'next/navigation';
export default async function Page({ params }) {
const post = await getPost(params.slug);
if (!post) notFound(); // 404 트리거
return <div>{post.title}</div>;
}
자동으로 브라우저 404 상태와 연동된다.
error페이지
에러페이지는 렌더링 중 예외 처리용으로 사용된다.
- React 컴포넌트 내부에서 발생하는 런타임 에러 처리
- API 오류, 렌더링 중 상태 에러 등 전반적인 예외 대응
// app/error.tsx
'use client';
export default function GlobalError({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>에러 발생!</h2>
<pre>{error.message}</pre>
<button onClick={reset}>다시 시도</button>
</div>
);
}
자동으로 ErrorBoundary 역할을 하며,
클라이언트 컴포넌트여야 하므로 use client 필수이다.
error페이지 vs not-found페이지
| 구분 | error.tsx |
not-found.tsx |
|---|---|---|
| 처리 대상 | 런타임 에러, 예외 (JS 오류) | 404 Not Found (존재하지 않는 페이지) |
| 호출 방식 | 자동 또는 throw new Error() |
자동 또는 notFound() 함수 호출 |
| 예시 상황 | API 실패, 컴포넌트 오류 등 | 없는 slug, 페이지 없음 등 |
| 사용 위치 | 글로벌 or 특정 폴더 | 글로벌 or 특정 폴더 |
| 재시도 기능 | reset() 제공 (컴포넌트 재렌더링) |
없음 |
| 클라이언트 컴포넌트 필요 | 예 (React ErrorBoundary) | 아니오 (서버 컴포넌트 가능) |
Next.js(App Router 기준)에서 404 외의 다른 에러들(500, 클라이언트 렌더링 에러, 비동기 에러 등)에 대응하는 방법
| 상황 | 대응 방식 | 위치 |
|---|---|---|
| 전역 렌더링 에러 | app/error.tsx |
최상위 |
| 특정 경로 에러 | app/경로/error.tsx |
폴더별 |
| 404 페이지 | app/not-found.tsx |
최상위 |
| API 서버 에러 | try/catch + Response |
API Route |
| 동적 라우팅 404 | notFound() 함수 |
서버 컴포넌트 내부 |
| 클라이언트 컴포넌트 | ErrorBoundary 사용 |
필요 위치 |
3. SEO-friendly HTML 구조 작성
SEO-friendly HTML 구조란, 검색엔진이 페이지를 정확하고 효율적으로 읽고 이해할 수 있도록 작성된 HTML 구조를 의미한다.
즉, 콘텐츠의 의미와 계층 구조를 명확하게 전달해서 검색 결과에 잘 노출되도록 돕는 구조다.
| 항목 | 설명 |
|---|---|
| 의미 있는 시맨틱 태그 사용 | <header>, <nav>, <main>, <section>, <article>, <footer> 등 HTML5 시맨틱 태그를 사용하여 구조를 명확히 함 |
| 계층 구조 준수 | 제목은 <h1> → <h2> → <h3> 순서로 사용해야 하며, <h1>은 페이지당 보통 1개만 사용 |
| alt 속성 추가 | <img> 태그에는 항상 alt 속성을 추가하여 이미지의 의미를 전달 |
링크는 <a>로 |
버튼이 아닌 경우에는 a 태그로 링크를 걸고, href는 절대 빠지면 안 됨(Next에서는 Link임) |
| 중복 콘텐츠 제거 | 같은 내용을 여러 페이지에서 반복 노출하지 않도록 주의 |
| 텍스트 콘텐츠 확보 | 중요한 내용은 이미지가 아닌 텍스트로 제공해야 크롤러가 인식 가능 |
예시
// 예시
export default function Page() {
return (
<>
<header>
<h1>보드게임 추천 서비스</h1>
<nav>
<ul>
<li><a href="/games">게임 목록</a></li>
<li><a href="/about">서비스 소개</a></li>
</ul>
</nav>
</header>
<main>
<section>
<h2>인기 게임</h2>
<article>
<h3>카탄</h3>
<p>전략과 협상을 즐기는 고전 명작</p>
</article>
</section>
</main>
<footer>
<p>© 2025 보드게임 서비스</p>
</footer>
</>
);
}
피할것
<div class="header">
<div class="menu">...</div>
</div>
<div class="body">
<div class="title">보드게임 추천</div>
<div class="text">카탄은 ...</div>
</div>
검색엔진 최적화 관점에서 좋지 않은 구조 예시다.
의미가 불분명하기때문이다.
검색엔진은 div만 보고 구조나 내용을 제대로 이해하지 못한다.
'기록 > 개발일기' 카테고리의 다른 글
| WIL: Date Hydration error, git 다른브랜치에서 분기 해결, 커밋순서 변경 (4) | 2025.08.06 |
|---|---|
| WIL: 백-프론트 변수네이밍, Breadcrumbs 컴포넌트, Partial 쓸까?, 별점 컴포넌트, 바텀시트 (2) | 2025.08.01 |
| WIL: 웹 접근성, Grid 컴포넌트, 컴파운드컴포넌트 패턴, Range 컴포넌트 (5) | 2025.07.30 |