스위프 9기 팀 프로젝트에서 카카오 OAuth 로그인을 구현하면서 겪은 과정과,
Next.js의 App Router 환경에서 발생한 빌드 오류 및 해결 과정을 정리했습니다.
로그인 버튼 클릭 시 카카오 OAuth 인증 페이지로 이동
로그인 완료 후 redirect URI에 code가 포함되어 돌아옴
이 code를 백엔드로 전달하여 로그인 완료 처리
로그인 후 홈으로 이동
export const getKakaoAuthURL = () => {
const baseUrl = "https://kauth.kakao.com/oauth/authorize";
const config = {
client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!,
redirect_uri: "http://localhost:3000/oauth/kakaocallback",
response_type: "code",
};
return `${baseUrl}?${new URLSearchParams(config).toString()}`;
};
🔍 설명
client_id, redirect_uri, response_type(code)을 쿼리로 포함"use client";
import { getKakaoAuthURL } from "@/api/oauth/kakao";
import Image from "next/image";
import kakao from "@/assets/images/kakao_login.png";
export default function LoginPage() {
const handleKakaoLogin = () => {
window.location.href = getKakaoAuthURL();
};
return (
<div className="flex h-screen items-center justify-center">
<button
onClick={handleKakaoLogin}
type="button"
className="cursor-pointer"
>
<Image src={kakao} alt="kakao" />
</button>
</div>
);
}
🔍 설명
처음엔 다음과 같이 구성했습니다:
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
export default function KakaoCallbackPage() {
const searchParams = useSearchParams(); // URL에서 code 추출
const code = searchParams.get('code');
const router = useRouter();
useEffect(() => {
if (code) {
// code를 백엔드에 전달 후 로그인 처리
router.push('/');
}
}, [code]);
return <div>로그인 처리 중입니다...</div>;
}
❗ 문제가 된 부분
useSearchParams() should be wrapped in a suspense boundary at page "/oauth/kakaocallback"
Error occurred prerendering page "/oauth/kakaocallback"
useSearchParams()는 브라우저에서만 실행 가능한 훅인데
Next.js는 이 페이지가 SSG 가능하다고 판단하고 서버에서 미리 렌더링하려 시도 → 실패
수정된 page.tsx (라우팅 페이지)
// app/(routes)/oauth/kakaocallback/page.tsx
export const dynamic = 'force-dynamic'; // ❗ 이 페이지는 정적 렌더링하지 마라!
import KakaoCallbackClient from '@/components/pages/oauth/KakaoCallbackClient';
export default function KakaoCallbackPage() {
return <KakaoCallbackClient />;
}
🔍 설명
export const dynamic = 'force-dynamic'를 선언하면 Next.js는 SSG 시도를 하지 않음
해당 선언은 import 아래에 위치해야 인식됨
실질적인 클라이언트 로직은 컴포넌트로 분리함
// components/pages/oauth/KakaoCallbackClient.tsx
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useOauthLogin } from '@/hooks/mutations/useOauthLogin';
export default function KakaoCallbackClient() {
const searchParams = useSearchParams();
const code = searchParams.get('code');
const router = useRouter();
const { mutate } = useOauthLogin();
useEffect(() => {
if (code) {
mutate(
{ provider: 'kakao', code },
{
onSuccess: () => {
router.push('/');
},
}
);
}
}, [code]);
return <div className="mt-20 text-center">로그인 처리 중입니다...</div>;
}
🔍 설명
useSearchParams()는 브라우저 환경에서만 실행되어야 하기 때문에 'use client'로 분리
code를 추출한 뒤 백엔드에 전달하고 로그인 상태를 갱신
이후 홈으로 리디렉션
<Suspense>로 감싸면 다음과 같이 렌더링 순서를 지연할 수 있습니다:
<Suspense fallback={<div>로딩 중...</div>}>
<KakaoCallbackClient />
</Suspense>
하지만 ❗ Next.js는 여전히 해당 페이지를 SSG 대상으로 간주하여
서버에서 렌더링을 시도하고 useSearchParams()를 실행 → 빌드 오류 발생
Suspense는 렌더 타이밍 조절 용도일 뿐, SSG 판단에는 영향을 주지 못함