모각 프로젝트에서는 카카오 소셜 로그인과 네이버 소셜 로그인을 지원한다.


관리자 및 테스트 로그인을 제외하고는 소셜 로그인으로만 회원가입 및 로그인이 가능하다.
이전 프로젝트에서 구현했던 로그인과 가장 다른 점은 next 의 서버사이드에서 처리했다는 것!
"use client";
import SubTitle from "@/app/components/shared/sub-title";
import kakaoLogin from "./kakao_login.png";
import naverLogin from "./naver_login.png";
import Link from "next/link";
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { getClientUser } from "@/utils/client/user.client.util";
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const nextPath = searchParams.get("next") || "/";
useEffect(() => {
if (getClientUser()) {
router.push("/");
}
}, []);
const handleKakaoLogin = () => {
window.location.href = `/api/internal/auth/kakao/login?next=${encodeURIComponent(
nextPath
)}`;
};
const handleNaverLogin = () => {
window.location.href = `/api/internal/auth/naver/login?next=${encodeURIComponent(
nextPath
)}`;
};
return (
<div>
<SubTitle contents="로그인" />
<span className="flex justify-center items-center mt-4 text-center">
소셜 로그인을 통해 간편하게 로그인 / 회원가입 해 보세요!
</span>
<div className="flex flex-col items-center m-10 justify-center gap-y-5">
<img
className="w-60 cursor-pointer"
src={kakaoLogin.src}
alt="카카오 로그인"
onClick={handleKakaoLogin}
/>
<img
className="w-60 cursor-pointer"
src={naverLogin.src}
alt="네이버 로그인"
onClick={handleNaverLogin}
/>
</div>
<hr className="w-80 mx-auto dark:border-border-dark border-borders" />
<div className="flex text-sm text-borders dark:text-border-dark justify-center items-center mt-4 text-center">
관리자 페이지는
<Link
className="mx-2 underline italic text-secondary dark:text-secondary-dark"
href="/login/admin"
>
여기
</Link>
를 클릭해 주세요.
</div>
</div>
);
}
기존 react+typescript 환경에서 로그인 후 로그인 이전 페이지로 돌아가는 기능 구현을 위해 쿠키를 사용했었는데,
SSR 이 섞인 Next 환경에서는 쿠키가 아닌 url 쿼리스트링을 사용하여 구현했다.
import { NextResponse } from "next/server";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const next = searchParams.get("next") || "/";
const clientId = process.env.KAKAO_REST_API_KEY!;
const redirectUri = process.env.KAKAO_REDIRECT_URI!;
const kakaoUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${encodeURIComponent(
next
)}`;
return NextResponse.redirect(kakaoUrl);
}
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
try {
const url = new URL(req.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const next = state || "/";
if (!code) {
return NextResponse.json({ error: "No code provided" }, { status: 400 });
}
const tokenRes = await fetch("https://kauth.kakao.com/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: process.env.KAKAO_REST_API_KEY!,
redirect_uri: process.env.KAKAO_REDIRECT_URI!,
code,
client_secret: process.env.KAKAO_CLIENT_SECRET!,
}),
});
const tokenData = await tokenRes.json();
if (!tokenData.access_token) {
return NextResponse.json(
{ error: "Token fetch failed", tokenData },
{ status: 500 }
);
}
const accessToken = tokenData.access_token;
const userRes = await fetch("https://kapi.kakao.com/v2/user/me", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const userData = await userRes.json();
const userInfo = {
provider: "kakao",
providerId: userData.id,
};
const res = await fetch(
`${process.env.BACKEND_API_URL}/auth/social-login`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userInfo),
}
);
const { token, memberId } = await res.json();
const redirectUrl = `${
process.env.FRONTEND_API_URL
}/login/callback?memberId=${memberId}&token=${token}&next=${encodeURIComponent(
next
)}`;
const response = NextResponse.redirect(redirectUrl);
response.cookies.set("jwt", token, { path: "/", httpOnly: false });
response.cookies.set("provider", "kakao");
return response;
} catch (err) {
console.error("카카오 로그인 처리 오류:", err);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
이렇게 /api (서버 사이드) 경로에서 외부 통신을 마친 다음, 지정한 callback 페이지로 redirect 한다.
"use client";
import { useEffect } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import Loading from "@/app/loading";
import { useAuthStore } from "@/store/authStore";
export default function AuthCallbackPage() {
const params = useSearchParams();
const router = useRouter();
const { login } = useAuthStore();
useEffect(() => {
const memberId = params.get("memberId");
const token = params.get("token");
const next = params.get("next") || "/";
if (memberId) {
localStorage.setItem("memberId", memberId);
}
if (token) {
localStorage.setItem("jwt", token);
login(token);
}
window.dispatchEvent(new Event("member:changed"));
router.replace(next);
}, [params, router, login]);
return <Loading />;
}
그러면 해당 callback 페이지에서 localStorage 에 memberId 와 jwt 를 저장하고,
쿼리스트링으로 계속 가지고 다닌 url 로 replace 한다.
위 예시는 카카오 로그인이며, 네이버 로그인도 크게 다르지 않으므로 줄이겠다.
'Dazz6 > ABOUT' 카테고리의 다른 글
| 모각 프로젝트 - #3. 배포 및 회고 (0) | 2025.08.07 |
|---|---|
| 모각 프로젝트 - #2-6. 관리자 페이지 (3) | 2025.08.06 |
| 모각 프로젝트 - #2-4. 비밀 고민 상담 (0) | 2025.08.04 |
| 모각 프로젝트 - #2-3. 모각챌 (0) | 2025.08.03 |
| 모각 프로젝트 - #2-2. 모각존 (0) | 2025.08.02 |