[feat] 소셜 로그인 OAuth 인증을 프론트엔드에서 직접 처리 - #248
Conversation
📝 Walkthrough요약프론트엔드에서 소셜 로그인 OAuth 인증을 직접 처리하도록 변경되었습니다. 기존 백엔드 리다이렉트 URL 기반 방식에서 구글은 Identity Services로 액세스 토큰을 직접 획득하고, 카카오는 인가 코드 플로우로 서버 측 토큰 교환을 수행하는 방식으로 전환되었습니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
actor User
participant Frontend as 프론트엔드
participant GoogleSDK as Google<br/>Identity Services
participant BackendAuth as 백엔드<br/>/auth/token
participant BrowserStorage as 쿠키 저장소
User->>Frontend: 구글 로그인 클릭
Frontend->>GoogleSDK: initTokenClient 초기화
GoogleSDK-->>Frontend: TokenClient 반환
Frontend->>GoogleSDK: requestAccessToken()
GoogleSDK->>User: 로그인 팝업 표시
User->>GoogleSDK: 자격증명 제공
GoogleSDK-->>Frontend: TokenResponse (access_token)
Frontend->>BackendAuth: POST { accessToken, refreshToken }
BackendAuth->>BackendAuth: 토큰 검증 및 처리
BackendAuth-->>BrowserStorage: httpOnly 쿠키 설정
BackendAuth-->>Frontend: 응답
Frontend->>Frontend: 홈 페이지 이동
sequenceDiagram
actor User
participant Frontend as 프론트엔드
participant KakaoAuth as 카카오<br/>인증 서버
participant BackendKakao as 백엔드<br/>/api/oauth/kakao
participant BackendAuth as 백엔드<br/>/auth/token
participant BrowserStorage as 쿠키 저장소
User->>Frontend: 카카오 로그인 클릭
Frontend->>KakaoAuth: 인가 URL로 리다이렉트<br/>(response_type=code)
KakaoAuth->>User: 로그인 페이지 표시
User->>KakaoAuth: 자격증명 제공
KakaoAuth-->>Frontend: 인가 코드와 함께 콜백
Frontend->>BackendKakao: GET /api/oauth/kakao?code=...
BackendKakao->>KakaoAuth: POST (code 교환)
KakaoAuth-->>BackendKakao: access_token 응답
BackendKakao-->>Frontend: { accessToken }
Frontend->>BackendAuth: POST loginWithKakao(accessToken)
BackendAuth->>BackendAuth: 토큰 검증 및 처리
BackendAuth-->>BrowserStorage: httpOnly 쿠키 설정
BackendAuth-->>Frontend: { accessToken, refreshToken }
Frontend->>Frontend: 홈 페이지 이동
예상 코드 리뷰 소요시간🎯 4 (복잡함) | ⏱️ ~50분 제안 검토자
축하 시
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/app/oauth/callback/kakao/page.tsx (1)
56-58: 🧹 Nitpick | 🔵 Trivialcatch 블록에서 에러 로깅 누락
에러가 발생해도 로깅 없이 로그인 페이지로 리다이렉트됩니다. 디버깅을 위해 에러 로깅을 추가하는 것이 좋습니다.
♻️ 에러 로깅 추가
- } catch { + } catch (error) { + console.error("Kakao login callback failed:", error); router.replace(ROUTES.login); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/oauth/callback/kakao/page.tsx` around lines 56 - 58, catch 블록에서 발생한 예외를 로깅하지 않고 바로 router.replace(ROUTES.login)만 호출하고 있어 디버깅 정보가 사라집니다; 해당 catch 블록(try/catch around the Kakao OAuth callback handling, where router.replace(ROUTES.login) is called)에서 캐치된 에러를 캡처해 적절한 로거나 console.error에 기록하도록 추가하고, 로그 메시지에 문맥(예: "Kakao OAuth callback error")과 에러 객체를 포함하도록 수정하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/package.json`:
- Around line 46-47: Remove the partial custom declaration for the
google.accounts.oauth2 namespace from global.d.ts to avoid duplicate/conflicting
types with the installed `@types/google.accounts` package; locate any "declare
namespace google.accounts.oauth2" (or related interface/type overrides) in
global.d.ts and delete those declarations so the project uses the official types
from `@types/google.accounts`, then run a TypeScript build/IDE reload to confirm
no missing types remain (if any missing APIs are actually used, add minimal
augmentation files that use module augmentation rather than redeclaring the
whole namespace).
In `@apps/web/src/_pages/auth/use-cases/social-login.ts`:
- Around line 15-16: When the fetch response check throws on (!res.ok) the error
lacks HTTP status and body details; update the failure path where the code
currently does "if (!res.ok) throw new Error('구글 로그인 실패')" to read the response
body (await res.text() or await res.json() safely) and include res.status and
the parsed error text/JSON in the thrown Error (or attach them to a custom Error
object) so the thrown error contains both HTTP status and server error message
for debugging.
- Line 9: Normalize API_BASE_URL to remove any trailing slashes before it’s used
so the fetch in social-login.ts (the expression using API_BASE_URL and TEAM_ID,
i.e., the fetch call to `${API_BASE_URL}/${TEAM_ID}/oauth/google`) and any
HttpClient URL concatenation never produce double slashes; implement this by
trimming trailing slashes from the API_BASE_URL value when it’s
initialized/derived (e.g., replace trailing / characters via a regex) and then
use that normalized constant in the fetch and HttpClient construction.
In `@apps/web/src/app/api/oauth/kakao/route.ts`:
- Around line 29-31: The code extracts access_token from tokenRes.json() but
doesn't validate it; update the block after const { access_token } = await
tokenRes.json() to check that access_token exists and is a non-empty string and,
if not, return a proper error response (e.g., NextResponse.json({ error:
'Missing access_token from Kakao' }, { status: 400 }) or throw a clear error)
instead of returning { accessToken: undefined }; reference tokenRes,
access_token, and NextResponse.json when making this change.
- Around line 18-21: The code uses non-null assertions for
NEXT_PUBLIC_KAKAO_CLIENT_ID, KAKAO_CLIENT_SECRET, and
NEXT_PUBLIC_KAKAO_REDIRECT_URI when building the token request, which can pass
undefined to the Kakao API; add an explicit runtime check at the start of the
route handler (the function that performs the token exchange in route.ts) to
validate process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID,
process.env.KAKAO_CLIENT_SECRET, and process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI
are present, remove the non-null assertions, and if any are missing return a
clear 500/4xx response or throw an error with a descriptive message so missing
environment variables are detected immediately. Ensure the validation runs
before using those values in the token request payload or calling any Kakao API.
In `@apps/web/src/features/auth/ui/login-form.tsx`:
- Around line 122-124: The redirect URL assignment that sets
window.location.href uses process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI raw in the
template string which can break if it contains special characters; update the
construction where window.location.href is set (in the click handler inside
login-form.tsx) to pass process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI through
encodeURIComponent (you can leave NEXT_PUBLIC_KAKAO_CLIENT_ID as-is) so the
redirect_uri query parameter is URL-encoded and safe.
- Around line 100-116: The Google OAuth callback lacks error handling: wrap the
callback logic passed to window.google.accounts.oauth2.initTokenClient (the
async callback) in a try/catch and handle failures from loginWithGoogle and the
fetch to "/api/auth/token"; on error, stop the redirect to ROUTES.home, log the
error (or report to process logger), and surface user feedback (e.g., set an
error state or show a toast) so the user isn't left waiting after
client.requestAccessToken() fails. Ensure any thrown errors from
loginWithGoogle(response.access_token) and await fetch(...) are caught and
handled gracefully.
In `@apps/web/src/features/auth/ui/signup-form.tsx`:
- Around line 129-145: The Google OAuth onClick duplicates login-form.tsx and
lacks error handling; extract the flow into a reusable hook (e.g.,
useGoogleLogin returning handleGoogleLogin) that calls
window.google.accounts.oauth2.initTokenClient and client.requestAccessToken, and
inside the callback wrap logic in try/catch: check response.error, call
loginWithGoogle(response.access_token), POST the tokens and verify res.ok, throw
or set an error state on any failure, and only call
window.location.replace(ROUTES.home) on success; replace the inline onClick to
call handleGoogleLogin.
- Around line 150-153: The Kakao OAuth redirect_uri in the onClick handler
(window.location.href) of signup-form.tsx is not URL-encoded; update the
construction of the authorization URL used in the onClick (the anonymous click
handler in the signup form component) to wrap
process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI with encodeURIComponent so the
redirect_uri parameter is properly encoded (mirroring the fix applied in
login-form.tsx).
In `@apps/web/src/global.d.ts`:
- Around line 8-10: The TokenResponse interface in global.d.ts is incomplete;
extend the TokenResponse interface (interface TokenResponse) to include optional
OAuth error fields so error cases can be handled—add at minimum optional
properties like error?: string and error_description?: string (you may also add
other common fields such as expires_in?: number, token_type?: string,
refresh_token?: string) so callers checking TokenResponse can safely inspect
both success and error responses.
- Around line 1-27: Remove the custom duplicate ambient declarations for
google.accounts.oauth2 (TokenClient, TokenResponse, initTokenClient and the
Window.google augmentation) from apps/web/src/global.d.ts and rely on the
installed `@types/google.accounts` package instead; delete the redundant
interfaces and initTokenClient declaration (or remove the entire file if it only
contains these declarations), ensure tsconfig includes the package types (remove
any manual triple-slash or conflicting declarations), and keep references to
TokenClient/TokenResponse/initTokenClient in code to use the official types
provided by the package.
---
Outside diff comments:
In `@apps/web/src/app/oauth/callback/kakao/page.tsx`:
- Around line 56-58: catch 블록에서 발생한 예외를 로깅하지 않고 바로 router.replace(ROUTES.login)만
호출하고 있어 디버깅 정보가 사라집니다; 해당 catch 블록(try/catch around the Kakao OAuth callback
handling, where router.replace(ROUTES.login) is called)에서 캐치된 에러를 캡처해 적절한 로거나
console.error에 기록하도록 추가하고, 로그 메시지에 문맥(예: "Kakao OAuth callback error")과 에러 객체를
포함하도록 수정하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c3d3f875-9107-4587-91d3-cab6d0d91ae5
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
apps/web/package.jsonapps/web/src/_pages/auth/use-cases/social-login-url.tsapps/web/src/_pages/auth/use-cases/social-login.tsapps/web/src/app/api/oauth/kakao/route.tsapps/web/src/app/layout.tsxapps/web/src/app/oauth/callback/kakao/page.tsxapps/web/src/features/auth/ui/login-form.tsxapps/web/src/features/auth/ui/signup-form.tsxapps/web/src/global.d.ts
💤 Files with no reviewable changes (1)
- apps/web/src/_pages/auth/use-cases/social-login-url.ts
| if (!res.ok) throw new Error("구글 로그인 실패"); | ||
| return res.json() as Promise<{ accessToken: string; refreshToken: string }>; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
에러 응답 디버깅 정보 부족
res.ok가 false일 때 응답 상태 코드나 에러 메시지를 포함하지 않아 디버깅이 어려울 수 있습니다.
♻️ 에러 정보 포함 제안
- if (!res.ok) throw new Error("구글 로그인 실패");
+ if (!res.ok) {
+ const errorBody = await res.text().catch(() => "");
+ throw new Error(`구글 로그인 실패: ${res.status} ${errorBody}`);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!res.ok) throw new Error("구글 로그인 실패"); | |
| return res.json() as Promise<{ accessToken: string; refreshToken: string }>; | |
| if (!res.ok) { | |
| const errorBody = await res.text().catch(() => ""); | |
| throw new Error(`구글 로그인 실패: ${res.status} ${errorBody}`); | |
| } | |
| return res.json() as Promise<{ accessToken: string; refreshToken: string }>; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/_pages/auth/use-cases/social-login.ts` around lines 15 - 16,
When the fetch response check throws on (!res.ok) the error lacks HTTP status
and body details; update the failure path where the code currently does "if
(!res.ok) throw new Error('구글 로그인 실패')" to read the response body (await
res.text() or await res.json() safely) and include res.status and the parsed
error text/JSON in the thrown Error (or attach them to a custom Error object) so
the thrown error contains both HTTP status and server error message for
debugging.
| client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!, | ||
| client_secret: process.env.KAKAO_CLIENT_SECRET!, | ||
| code, | ||
| redirect_uri: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!, |
There was a problem hiding this comment.
환경변수 미설정 시 런타임 오류 발생 가능
process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!, process.env.KAKAO_CLIENT_SECRET! 등에 non-null assertion을 사용하고 있으나, 환경변수가 설정되지 않은 경우 undefined가 그대로 전달되어 카카오 API 호출이 실패합니다. 서버 시작 시 또는 요청 처리 전에 검증이 필요합니다.
🛡️ 환경변수 검증 추가 제안
export async function GET(request: NextRequest) {
+ const clientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID;
+ const clientSecret = process.env.KAKAO_CLIENT_SECRET;
+ const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI;
+
+ if (!clientId || !clientSecret || !redirectUri) {
+ console.error("Missing Kakao OAuth environment variables");
+ return NextResponse.json({ message: "서버 설정 오류" }, { status: 500 });
+ }
+
const { searchParams } = new URL(request.url);
const code = searchParams.get("code");
// ... 이하 동일
body: new URLSearchParams({
grant_type: "authorization_code",
- client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!,
- client_secret: process.env.KAKAO_CLIENT_SECRET!,
+ client_id: clientId,
+ client_secret: clientSecret,
code,
- redirect_uri: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!,
+ redirect_uri: redirectUri,
}),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/app/api/oauth/kakao/route.ts` around lines 18 - 21, The code
uses non-null assertions for NEXT_PUBLIC_KAKAO_CLIENT_ID, KAKAO_CLIENT_SECRET,
and NEXT_PUBLIC_KAKAO_REDIRECT_URI when building the token request, which can
pass undefined to the Kakao API; add an explicit runtime check at the start of
the route handler (the function that performs the token exchange in route.ts) to
validate process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID,
process.env.KAKAO_CLIENT_SECRET, and process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI
are present, remove the non-null assertions, and if any are missing return a
clear 500/4xx response or throw an error with a descriptive message so missing
environment variables are detected immediately. Ensure the validation runs
before using those values in the token request payload or calling any Kakao API.
There was a problem hiding this comment.
배포 환경에서 환경변수가 항상 설정되어 있어서 별도로 런타임 검증하는 건 불필요하다고 판단함.
| onClick={() => { | ||
| window.location.href = getGoogleLoginUrl(); | ||
| // 구글 팝업으로 access_token 획득 후 백엔드로 전달 | ||
| const client = window.google.accounts.oauth2.initTokenClient({ | ||
| client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!, | ||
| scope: "email profile", | ||
| callback: async (response) => { | ||
| const { accessToken, refreshToken } = await loginWithGoogle(response.access_token); | ||
| await fetch("/api/auth/token", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ accessToken, refreshToken }), | ||
| }); | ||
| window.location.replace(ROUTES.home); | ||
| }, | ||
| }); | ||
| client.requestAccessToken(); | ||
| }} |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
login-form.tsx와 동일한 에러 처리 누락 및 코드 중복
Google OAuth 콜백에 에러 처리가 없으며, login-form.tsx와 완전히 동일한 코드가 중복되어 있습니다.
♻️ 공통 훅 또는 유틸리티로 추출 제안
소셜 로그인 로직을 커스텀 훅으로 추출하여 재사용하세요:
// hooks/use-google-login.ts
export function useGoogleLogin() {
const handleGoogleLogin = useCallback(() => {
const client = window.google.accounts.oauth2.initTokenClient({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
scope: "email profile",
callback: async (response) => {
try {
if (response.error) throw new Error(response.error);
const { accessToken, refreshToken } = await loginWithGoogle(response.access_token);
const res = await fetch("/api/auth/token", { /* ... */ });
if (!res.ok) throw new Error("Token storage failed");
window.location.replace(ROUTES.home);
} catch (error) {
console.error("Google login failed:", error);
// 에러 상태 업데이트
}
},
});
client.requestAccessToken();
}, []);
return { handleGoogleLogin };
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/features/auth/ui/signup-form.tsx` around lines 129 - 145, The
Google OAuth onClick duplicates login-form.tsx and lacks error handling; extract
the flow into a reusable hook (e.g., useGoogleLogin returning handleGoogleLogin)
that calls window.google.accounts.oauth2.initTokenClient and
client.requestAccessToken, and inside the callback wrap logic in try/catch:
check response.error, call loginWithGoogle(response.access_token), POST the
tokens and verify res.ok, throw or set an error state on any failure, and only
call window.location.replace(ROUTES.home) on success; replace the inline onClick
to call handleGoogleLogin.
There was a problem hiding this comment.
에러 처리는 try/catch + toast로 이미 반영함. 공통 훅 추출은 login과 signup의 토스트 메시지와 리다이렉트 경로가 달라서 훅으로 묶으면 오히려 복잡해질 것 같아 그대로 유지하겠음.
📌 Summary
소셜 로그인 방식을 백엔드 리다이렉트 방식에서 프론트엔드 토큰 직접 전달 방식으로 변경
기존에는 백엔드(
together-dallaem-api.vercel.app)가 구글/카카오 로그인 페이지로 리다이렉트하는 방식로그인 동의 화면에 "같이달램", "together-dallaem-api.vercel.app"이 표시되는 문제가 있었음
서비스명인 "moum-zip"으로 표시하기 위해 카카오 디벨로퍼스와 구글 클라우드 콘솔에서 moum-zip 전용 앱을 새로 생성하고, 프론트에서 직접 OAuth 인증을 처리하는 방식으로 변경
close [feat] 소셜 로그인 OAuth 인증을 프론트엔드에서 직접 처리 #244
📄 Tasks
소셜 로그인 로직 교체
social-login-url.ts삭제 ->social-login.ts로 교체POST /{teamId}/oauth/google,POST /{teamId}/oauth/kakao)로 전달하는 함수로 교체Google 로그인
login-form.tsx,signup-form.tsxonClick 핸들러 교체initTokenClient)로 구글 팝업을 띄워 access_token 획득/api/auth/token으로 httpOnly 쿠키에 저장layout.tsx에 Google Identity Services SDK 스크립트 추가global.d.ts추가:window.google타입 선언@types/google.accounts패키지로 타입 자동 인식@types/google.accounts에 없는error,error_description필드 추가Kakao 로그인
login-form.tsx,signup-form.tsxonClick 핸들러 교체app/api/oauth/kakao/route.ts신규 추가oauth/callback/page.tsx->oauth/callback/kakao/page.tsx로 이동 및 Kakao 전용으로 수정/api/auth/token으로 쿠키 저장기타
shared/config/env.ts:API_BASE_URLtrailing slash 정규화 (replace 사용)변경 없음
app/api/auth/token/route.ts: httpOnly 쿠키 저장 로직 그대로 재사용👀 To Reviewer
callback함수 내에서 쿠키 저장까지 처리합니다!.env.local에 아래 환경변수 추가가 필요합니다.NEXT_PUBLIC_GOOGLE_CLIENT_ID
NEXT_PUBLIC_KAKAO_CLIENT_ID
KAKAO_CLIENT_SECRET
NEXT_PUBLIC_KAKAO_REDIRECT_URI
📸 Screenshot
Summary by CodeRabbit
출시 노트
새로운 기능
Chores