-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/#53 카카오 소셜 로그인 프로세스 구현 #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- code 및 redirectUri 파라미터 포함
- route.ts로 서버에서 동작하도록 구현 (프록시 역할)
- api 요청 시 header에 accessToken 자동 삽입 (서버 / 클라이언트 요청 환경 구분)
- 인증이 필요한 페이지에서만 동작하도록 설정
- cookies.set에 따른 이전 쿠키 초기화 발생
- 백엔드 서버에서 도메인이 달라 쿠키 접근 불가로 인해 다른 방법으로 변경 예정
- next서버와 백엔드 서버 모두 쿠키에 접근이 가능하도록 proxy 역할을 하는 rewrites 추가 - route.ts 역할을 page.tsx로 함으로 브라우저에 바로 쿠키가 저장될 수 있도록 변경
- getToken에서 axiosInstance으로 요청 후 401 발생 시 무한 요청 발생 -> 일반 axios로 구현 후 예외 처리 따로 구현
Walkthrough클라이언트 측 카카오 로그인 플로우와 토큰 관리(쿠키 저장), Axios 요청/응답 인터셉터(액세스 토큰 자동 첨부 및 401 시 갱신), 별도 V2 axios 인스턴스, 글로벌 Window Kakao 타입, 로그인 UI 및 콜백 페이지/미들웨어, 리라이트 규칙 및 하이브리드 getCookie 유틸이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant LoginBtn as LoginButton
participant Kakao
participant SuccessPage as /login/loading/success
participant Backend
participant ClientApp
User->>LoginBtn: 클릭 -> getKakaoInga()
alt Android
LoginBtn->>Kakao: 리다이렉트 to Kakao OAuth URL
else Non-Android
LoginBtn->>Kakao: Kakao.Auth.authorize (JS SDK)
end
Kakao-->>SuccessPage: redirect with ?code=...
SuccessPage->>Backend: POST /auth/authorize (code, redirectUri) via axiosInstanceV2
Backend-->>SuccessPage: { accessToken, expiresIn, ... }
SuccessPage->>SuccessPage: 쿠키에 accessToken 저장
SuccessPage->>ClientApp: 리다이렉트 (메인)
Note over ClientApp: 이후 axiosInstance 요청은 요청 인터셉터가 Bearer 토큰 첨부
sequenceDiagram
participant Client
participant Middleware
participant Protected
participant Login
Client->>Middleware: 요청 (예: /user)
alt accessToken 쿠키 존재
Middleware->>Protected: 요청 허용
Protected-->>Client: 리소스 응답
else accessToken 없음
Middleware->>Login: 리다이렉트
Login-->>Client: 로그인 페이지
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 검토 시 추가로 주의할 파일/영역:
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Nitpick comments (9)
apps/web/middleware.ts (1)
5-14: 쿠키 이름을 상수로 추출하는 것을 고려하세요.현재 'accessToken' 문자열이 하드코딩되어 있습니다. 다른 파일에서도 동일한 쿠키 이름을 사용하는 경우 상수로 추출하면 유지보수성이 향상됩니다.
예시:
// apps/web/app/_constants/auth.ts export const COOKIE_NAMES = { ACCESS_TOKEN: 'accessToken', // ... } as constimport { NextRequest, NextResponse } from 'next/server' import { cookies } from 'next/headers' import { CLIENT_PATH } from 'app/_constants/path' + import { COOKIE_NAMES } from 'app/_constants/auth' export async function middleware(request: NextRequest) { const cookieStore = await cookies() - const accessToken = cookieStore.get('accessToken')?.value + const accessToken = cookieStore.get(COOKIE_NAMES.ACCESS_TOKEN)?.value if (!accessToken) { return NextResponse.redirect(new URL(CLIENT_PATH.LOGIN, request.url)) } return NextResponse.next() }apps/web/app/login/components/LoginButton.tsx (2)
9-17: Kakao SDK 초기화 시 환경 변수 누락 처리 필요
process.env.NEXT_PUBLIC_KAKAO_JS_KEY || ''때문에 키가 설정되지 않은 경우에도Kakao.init('')가 호출될 수 있어, 런타임 오류나 예기치 않은 동작을 유발할 수 있습니다.
키가 없으면 초기화를 건너뛰고 경고 로그를 남기거나, 빌드/런타임에서 명시적으로 에러를 내는 쪽이 더 안전해 보입니다.
19-27: 버튼 기본 type 지정으로 예기치 않은 form submit 방지이 컴포넌트가
<form>안에서 사용될 경우 기본type="submit"동작으로 인해 의도치 않게 폼이 제출될 수 있습니다.
로그인 트리거 용도라면type="button"을 명시해 두는 것이 안전합니다.apps/web/app/login/loading/success/_route.ts (1)
1-45: 전부 주석 처리된 라우트 핸들러 정리 제안현재 파일 전체가 주석 처리된 상태라 실제로는 어떤 동작도 하지 않습니다.
- 이 접근을 더 이상 사용하지 않을 계획이라면, 파일을 삭제하거나 간단한 설명/TODO 주석만 남기는 편이 혼동을 줄이는 데 좋습니다.
- 나중에 다시 사용할 예정이라면, 이 PR 설명에 적힌 “왜 보류했는지(도메인/쿠키 이슈)”를 짧게 요약해서 상단에 주석으로 남겨 두면 이해에 도움이 될 것 같습니다.
apps/web/app/global.d.ts (1)
1-10: Window.Kakao를 선택 속성으로 정의하는 편이 런타임과 더 일치현재 타입은
interface Window { Kakao: { ... } }로 Kakao가 항상 존재하는 것처럼 정의되어 있습니다.
실제 런타임에서는 SDK 스크립트가 로드되기 전까지window.Kakao가undefined일 수 있고,LoginButton에서도if (!window.Kakao) return으로 이를 가정하고 있습니다.아래처럼 선택 속성으로 두거나, 필요하면
null가능성을 포함해 실제 동작과 타입을 맞춰 주는 것을 권장합니다:interface Window { Kakao?: { init: (appKey: string) => void isInitialized: () => boolean Auth: { authorize: (params: Record<string, unknown>) => Promise<void> } [key: string]: unknown } }apps/web/app/_constants/path.ts (2)
42-46: AUTH.AUTHORIZE에서 redirectUri 쿼리 파라미터 인코딩 고려AUTHORIZE: (code: string, redirectUri: string) => `/auth/oauth2?code=${code}&redirectUri=${redirectUri}`,현재는
redirectUri를 그대로 쿼리 파라미터에 넣고 있어서,
- 추후
redirectUri에?,&등 추가 쿼리 파라미터가 붙는 경우- 혹은 특수 문자가 포함되는 경우
URL이 깨지거나 서버에서 정상 파싱하지 못할 수 있습니다.
보다 안전하게는 아래처럼
encodeURIComponent를 사용하는 쪽을 권장합니다:AUTHORIZE: (code: string, redirectUri: string) => `/auth/oauth2?code=${encodeURIComponent(code)}&redirectUri=${encodeURIComponent(redirectUri)}`
64-65: LOGIN_SUCCESS 경로 및 TODO 주석 정리 제안LOGIN_SUCCESS: '/login/loading/success', // Todo: path 변경 필요이미 TODO로 남겨두셨지만, 로그인/콜백 URL은 프론트, 백엔드, 카카오 콘솔 설정이 모두 맞아야 해서 나중에 변경 시 디버깅 비용이 꽤 큽니다.
- 이번 PR에서 가능한 한 최종적으로 사용할 경로를 확정해 두거나,
- 아니면 issue 번호를 함께 적어 (
// TODO(#53): ...) 추후 변경 시점을 명확히 해두면 좋겠습니다.apps/web/app/login/loading/success/page.tsx (1)
38-39: 로딩 상태 표시 고려.현재 컴포넌트는 아무것도 렌더링하지 않습니다. 인증 처리 중임을 사용자에게 알리는 로딩 인디케이터를 표시하면 사용자 경험이 개선될 수 있습니다.
다음과 같이 로딩 상태를 추가할 수 있습니다:
- return null + return ( + <div className="flex items-center justify-center min-h-screen"> + <div>로그인 처리 중...</div> + </div> + )apps/web/app/_apis/services/login.ts (1)
51-53: 클라이언트 전용 함수로 명확히 하는 것을 고려.
typeof window !== 'undefined'체크가 있지만, 이 함수는 클라이언트 사이드에서만 호출되는 것으로 보입니다. 함수 상단에 주석으로 명시하거나, 호출하는 컴포넌트가'use client'지시어를 사용하도록 하여 의도를 명확히 할 수 있습니다.함수 상단에 주석을 추가할 수 있습니다:
+/** + * Refreshes the access token using the refresh token cookie. + * @remarks This function should only be called from client-side components. + */ export const getToken = async (): Promise<{
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (15)
apps/web/app/_apis/services/login.ts(1 hunks)apps/web/app/_constants/path.ts(2 hunks)apps/web/app/_lib/axiosInstance.ts(1 hunks)apps/web/app/_lib/axiosInstanceV2.ts(1 hunks)apps/web/app/global.d.ts(1 hunks)apps/web/app/layout.tsx(1 hunks)apps/web/app/login/components/LoginButton.tsx(1 hunks)apps/web/app/login/loading/success/_route.ts(1 hunks)apps/web/app/login/loading/success/page.tsx(1 hunks)apps/web/app/login/page.tsx(1 hunks)apps/web/app/map/MapContainer.tsx(1 hunks)apps/web/app/map/_components/PlaceList/PlaceList.tsx(1 hunks)apps/web/middleware.ts(1 hunks)apps/web/next.config.ts(1 hunks)apps/web/package.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/app/login/components/LoginButton.tsx (1)
apps/web/app/_apis/services/login.ts (1)
getKakaoInga(8-22)
apps/web/app/_lib/axiosInstance.ts (1)
apps/web/app/_apis/services/login.ts (1)
getToken(24-57)
apps/web/app/login/loading/success/page.tsx (1)
apps/web/app/page.tsx (1)
Page(25-59)
apps/web/app/login/page.tsx (1)
apps/web/app/login/components/LoginButton.tsx (1)
LoginButton(8-28)
🔇 Additional comments (6)
apps/web/app/map/_components/PlaceList/PlaceList.tsx (1)
27-27: LGTM!하단 여백 추가로 리스트의 가독성이 개선되었습니다.
apps/web/app/map/MapContainer.tsx (1)
4-8: LGTM!텍스트 로딩 메시지를 Spinner 컴포넌트로 교체하여 사용자 경험이 개선되었습니다.
apps/web/app/login/page.tsx (1)
1-21: LGTM!로그인 페이지가 깔끔하게 구현되었습니다. 프레젠테이션 로직과 비즈니스 로직이 적절히 분리되어 있습니다.
apps/web/middleware.ts (1)
16-32: LGTM!matcher를 특정 보호 라우트로만 제한하여 불필요한 미들웨어 실행을 방지하는 좋은 접근 방식입니다.
apps/web/package.json (1)
28-28: cookies-next 패키지 버전 검증 완료 - 안전함검증 결과:
- 최신 버전: 6.1.1 (PR에서 사용 중인 버전과 일치)
- 보안 취약점: 없음 (GitHub Advisory Database 확인)
PR에서 사용하는 cookies-next@^6.1.1은 최신 버전이며 알려진 보안 취약점이 없으므로 안전합니다.
apps/web/app/_apis/services/login.ts (1)
30-36: 리뷰 의견이 이 파일에 적용되지 않습니다.
login.ts에서 직접axios를 사용하는 것은 의도된 아키텍처 결정입니다.getToken()함수가axiosInstance의 응답 인터셉터에서 호출되기 때문에,login.ts가axiosInstance를 사용하면 순환 의존성(circular dependency)이 발생합니다. 직접axios를 사용함으로써 이를 방지합니다.
axiosInstance는 요청 인터셉터에서 인증 토큰을 추가하고 응답 인터셉터에서 토큰 갱신을 처리하므로, 토큰 획득 엔드포인트는 이 메커니즘의 외부에 있어야 합니다.Likely an incorrect or invalid review comment.
| export const getKakaoInga = async () => { | ||
| const isAndroid = /Android/.test(navigator.userAgent) | ||
| // const isiOS = /(iPhone|iPad|iPod)/.test(navigator.userAgent); | ||
| try { | ||
| if (isAndroid) { | ||
| window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${CLIENT_URL + CLIENT_PATH.LOGIN_SUCCESS}&response_type=code` | ||
| } else { | ||
| await window.Kakao.Auth.authorize({ | ||
| redirectUri: CLIENT_URL + CLIENT_PATH.LOGIN_SUCCESS, | ||
| }) | ||
| } | ||
| } catch (error) { | ||
| console.log(error) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
타입 안전성 및 에러 처리 개선 필요.
이 함수에는 몇 가지 개선이 필요한 부분이 있습니다:
window.Kakao에 대한 타입 정의가 없어 타입 안전성이 부족합니다.- 에러가 발생해도 사용자에게 알리지 않고 콘솔에만 로그를 남깁니다.
- 주석 처리된 iOS 감지 코드는 제거하거나 구현을 완료해야 합니다.
타입 안전성을 위해 전역 타입 정의를 추가하세요 (예: types/kakao.d.ts):
interface Window {
Kakao: {
Auth: {
authorize: (options: { redirectUri: string }) => Promise<void>
}
}
}에러 처리를 개선하세요:
} catch (error) {
- console.log(error)
+ console.error('Kakao login failed:', error)
+ alert('로그인에 실패했습니다. 다시 시도해주세요.')
+ throw error
}🤖 Prompt for AI Agents
In apps/web/app/_apis/services/login.ts around lines 8 to 22, this function
lacks type safety for window.Kakao, swallows errors to console only, and leaves
commented iOS detection; add a global type declaration file (e.g.,
types/kakao.d.ts) that declares window.Kakao with Auth.authorize signature,
remove or implement the commented iOS detection (use the same regex as Android
counterpart), add a runtime guard to check window.Kakao before calling authorize
and fall back to the redirect URL for non-present SDKs, and replace console.log
with user-facing error handling (e.g., throw a typed error or call a toast/error
modal) while still logging the error details for diagnostics.
| axiosInstance.interceptors.response.use( | ||
| (response: AxiosResponse) => response.data, | ||
| async (error: AxiosError): Promise<AxiosResponse | AxiosError> => { | ||
| const originalRequest = error.config as InternalAxiosRequestConfig & { | ||
| _retry?: boolean | ||
| } | ||
| if (error.response?.status === 401 && !originalRequest._retry) { | ||
| originalRequest._retry = true | ||
|
|
||
| try { | ||
| const { accessToken: newAccessToken } = await getToken() | ||
|
|
||
| if (originalRequest.headers) { | ||
| originalRequest.headers.Authorization = newAccessToken | ||
| ? `Bearer ${newAccessToken}` | ||
| : '' | ||
| } | ||
|
|
||
| return await axiosInstance(originalRequest) | ||
| } catch (refreshError) { | ||
| return Promise.reject(refreshError) | ||
| } | ||
| } | ||
| return Promise.reject(error) | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
401 처리부의 getToken 반환 구조 및 타입 불일치 가능성
401 응답 처리 로직에서 다음과 같이 getToken 결과를 구조분해하고 있습니다:
const { accessToken: newAccessToken } = await getToken()하지만 제공된 getToken 구현 스니펫을 보면 return res.data를 반환하고 있어, 실제 구조가 { data: { accessToken, ... } } 형태일 수 있습니다. 이 경우:
newAccessToken은undefined가 되어 의도대로 Authorization 헤더를 설정하지 못할 수 있습니다.- 다만
getToken내부에서 이미setCookie('accessToken', accessToken)를 수행하므로, 재시도 요청에서는 request 인터셉터가 쿠키에서 토큰을 읽어와 보정해주는 형태가 됩니다. 즉 “우연히 동작하긴 하지만 코드 간의 계약이 어긋난 상태”입니다.
또한 이 인터셉터는 성공 시 response.data를 반환하지만, 에러 핸들러 시그니처는 Promise<AxiosResponse | AxiosError>로 선언되어 있어 실제 반환 타입과 어긋납니다. 나중에 제너릭 타입을 붙여 사용할 때 타입 추론이 꼬일 수 있습니다.
권장 사항:
getToken이 실제로 어떤 shape을 반환하는지(평탄화된{ accessToken, ... }인지,data.data구조인지)를 이 파일과 일치시키고, 타입 선언도 맞춰 주세요.- 이 인터셉터에서의 반환 타입도
AxiosResponse대신 “실제 data 타입”을 반영하도록 정리하는 편이 이후 DX에 도움이 됩니다.
getToken의 실제 반환 타입과 이곳 구조분해가 일치하는지, 그리고 axiosInstance를 사용하는 쪽에서 기대하는 응답 타입과 현재 인터셉터 반환값이 일관적인지 다시 한 번 점검해 주세요.
I'll verify the review comment by examining the getToken function, the interceptor type signatures, and how axiosInstance is used throughout the codebase.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the getToken function definition and its return type
fd -t f "\.(ts|tsx|js|jsx)$" | xargs rg -n "export.*function.*getToken|const.*getToken.*=" -A 10 --type=ts --type=tsx | head -100
Length of output: 27
🏁 Script executed:
#!/bin/bash
# Find imports and usages of axiosInstance to understand expected response types
fd -t f "\.(ts|tsx)$" | xargs rg -n "axiosInstance" -B 2 -A 5 --type=ts --type=tsx | head -150Length of output: 27
🏁 Script executed:
#!/bin/bash
# Check the full axios interceptor setup and response type declarations
cat apps/web/app/_lib/axiosInstance.tsLength of output: 1667
🏁 Script executed:
#!/bin/bash
# Search for getToken function and setCookie usage to verify the implementation
fd -t f "\.(ts|tsx)$" | xargs rg -n "setCookie.*accessToken|getToken" -B 3 -A 10 --type=ts --type=tsx | head -150Length of output: 27
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the login service file
fd -t f "login\.(ts|js)" --path "*/_apis/*"
Length of output: 233
🏁 Script executed:
#!/bin/bash
# Search for getToken in all TypeScript files using correct rg syntax
rg -n "export.*function.*getToken|export.*const.*getToken" -A 15 --type tsLength of output: 1017
🏁 Script executed:
#!/bin/bash
# Find all files in _apis directory
fd --path "*/_apis/*" -t fLength of output: 215
---
🏁 Script executed:
```shell
#!/bin/bash
# Get the complete getToken implementation
cat apps/web/app/_apis/services/login.ts
Length of output: 1582
🏁 Script executed:
#!/bin/bash
# Search for usages of axiosInstance in the codebase to check how responses are typed
rg -n "axiosInstance\." --type ts -A 3 -B 1 | head -100Length of output: 5814
---
🏁 Script executed:
```shell
#!/bin/bash
# Verify getToken's actual return type annotation matches implementation
rg -n "return res\.data" apps/web/app/_apis/services/login.ts -B 5 -A 2
Length of output: 191
🏁 Script executed:
#!/bin/bash
# Check if there are any test files or type validation for getToken
rg -n "getToken" apps/web/app --type ts | grep -E "test|spec|mock" | head -20Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look at how axiosInstance response is typed when used
rg -n "await axiosInstance" --type ts -B 2 -A 5 | head -80Length of output: 5030
getToken 반환 타입과 response interceptor 타입 선언 불일치 - 검증 완료
검증 결과 리뷰의 우려사항이 확인되었습니다:
-
getToken 반환 타입 불일치 (apps/web/app/_apis/services/login.ts 46줄):
- 선언된 타입:
Promise<{ tokenType: string; accessToken: string; accessTokenExpiresIn: number }> - 실제 반환값:
res.data(중첩 구조{ data: { accessToken, ... } }) - axiosInstance 라인 49의 구조분해
const { accessToken: newAccessToken } = await getToken()는 최상위 레벨의 accessToken을 기대하지만, 실제로는 undefined가 됨 - 요청 인터셉터에서 쿠키로부터 토큰을 읽어서 우연히 동작함
- 선언된 타입:
-
Response interceptor 타입 선언 오류 (apps/web/app/_lib/axiosInstance.ts 40줄):
- 에러 핸들러 시그니처:
Promise<AxiosResponse | AxiosError> - 실제 반환: 성공 시 data 객체, 실패 시 Promise.reject()
- 사용처들은
const { data } = await axiosInstance(...)로 구조분해하고 있음
- 에러 핸들러 시그니처:
필수 수정 사항:
getToken반환 타입을 실제 반환값과 일치시키거나, 구현을 타입과 맞춰서 평탄화된 객체를 반환하도록 수정- Response interceptor의 제너릭 타입 선언을 정리하여 실제 반환 타입과 일치
🤖 Prompt for AI Agents
In apps/web/app/_lib/axiosInstance.ts around lines 38 to 63, the response
interceptor and getToken usage are mismatched with actual shapes: getToken
currently returns a nested { data: { accessToken, ... } } but the interceptor
expects a flat { accessToken }, and the interceptor error handler return type
Promise<AxiosResponse | AxiosError> is incorrect for an instance that resolves
to response.data; fix by updating getToken to return the flattened shape
(Promise<{ tokenType: string; accessToken: string; accessTokenExpiresIn: number
}>) or change its caller here to extract accessToken from the nested data (e.g.,
const { data: { accessToken } } = await getToken()), and update the interceptor
typings to reflect the instance resolves to the data payload (adjust
generic/return types to the data type or Promise<any> and ensure the error
handler returns Promise.reject(error) on failure) so runtime behavior and
TypeScript signatures align.
| const axiosInstanceV2 = axios.create({ | ||
| baseURL: process.env.NEXT_PUBLIC_API_URL_V2, | ||
| withCredentials: true, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
환경 변수가 정의되지 않은 경우를 처리하세요.
NEXT_PUBLIC_API_URL_V2가 정의되지 않은 경우 axios 인스턴스의 baseURL이 undefined가 되어 모든 API 요청이 실패합니다.
다음과 같이 환경 변수 검증을 추가하세요:
import axios from 'axios'
+ const apiUrlV2 = process.env.NEXT_PUBLIC_API_URL_V2
+ if (!apiUrlV2) {
+ throw new Error('NEXT_PUBLIC_API_URL_V2 environment variable is not defined')
+ }
+
const axiosInstanceV2 = axios.create({
- baseURL: process.env.NEXT_PUBLIC_API_URL_V2,
+ baseURL: apiUrlV2,
withCredentials: true,
})
export default axiosInstanceV2📝 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.
| const axiosInstanceV2 = axios.create({ | |
| baseURL: process.env.NEXT_PUBLIC_API_URL_V2, | |
| withCredentials: true, | |
| }) | |
| import axios from 'axios' | |
| const apiUrlV2 = process.env.NEXT_PUBLIC_API_URL_V2 | |
| if (!apiUrlV2) { | |
| throw new Error('NEXT_PUBLIC_API_URL_V2 environment variable is not defined') | |
| } | |
| const axiosInstanceV2 = axios.create({ | |
| baseURL: apiUrlV2, | |
| withCredentials: true, | |
| }) | |
| export default axiosInstanceV2 |
🤖 Prompt for AI Agents
In apps/web/app/_lib/axiosInstanceV2.ts around lines 3 to 6, the axios instance
is created with baseURL set directly to process.env.NEXT_PUBLIC_API_URL_V2 which
can be undefined and cause all requests to fail; validate the environment
variable at module initialization and either throw a clear error (so the
developer notices misconfiguration) or set a sensible fallback, and then create
the axios instance using the validated value; ensure the validation uses a
single source (e.g. const BASE_URL = process.env.NEXT_PUBLIC_API_URL_V2) and
reference that when creating the instance so you don't risk passing undefined.
| async rewrites() { | ||
| return [ | ||
| { | ||
| source: '/api/v1/:path*', | ||
| destination: process.env.NEXT_PUBLIC_API_URL_BASE + '/api/v1/:path*', | ||
| }, | ||
| { | ||
| source: '/api/v2/:path*', | ||
| destination: process.env.NEXT_PUBLIC_API_URL_BASE + '/api/v2/:path*', | ||
| }, | ||
| ] | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
환경 변수가 정의되지 않은 경우를 처리하세요.
process.env.NEXT_PUBLIC_API_URL_BASE가 정의되지 않은 경우 rewrite destination이 'undefined/api/v1/:path*'가 되어 라우팅이 실패합니다.
다음과 같이 환경 변수 검증을 추가하세요:
async rewrites() {
+ const apiUrlBase = process.env.NEXT_PUBLIC_API_URL_BASE
+ if (!apiUrlBase) {
+ throw new Error('NEXT_PUBLIC_API_URL_BASE environment variable is not defined')
+ }
return [
{
source: '/api/v1/:path*',
- destination: process.env.NEXT_PUBLIC_API_URL_BASE + '/api/v1/:path*',
+ destination: `${apiUrlBase}/api/v1/:path*`,
},
{
source: '/api/v2/:path*',
- destination: process.env.NEXT_PUBLIC_API_URL_BASE + '/api/v2/:path*',
+ destination: `${apiUrlBase}/api/v2/:path*`,
},
]
},📝 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.
| async rewrites() { | |
| return [ | |
| { | |
| source: '/api/v1/:path*', | |
| destination: process.env.NEXT_PUBLIC_API_URL_BASE + '/api/v1/:path*', | |
| }, | |
| { | |
| source: '/api/v2/:path*', | |
| destination: process.env.NEXT_PUBLIC_API_URL_BASE + '/api/v2/:path*', | |
| }, | |
| ] | |
| }, | |
| async rewrites() { | |
| const apiUrlBase = process.env.NEXT_PUBLIC_API_URL_BASE | |
| if (!apiUrlBase) { | |
| throw new Error('NEXT_PUBLIC_API_URL_BASE environment variable is not defined') | |
| } | |
| return [ | |
| { | |
| source: '/api/v1/:path*', | |
| destination: `${apiUrlBase}/api/v1/:path*`, | |
| }, | |
| { | |
| source: '/api/v2/:path*', | |
| destination: `${apiUrlBase}/api/v2/:path*`, | |
| }, | |
| ] | |
| }, |
🤖 Prompt for AI Agents
In apps/web/next.config.ts around lines 19 to 30, the rewrites use
process.env.NEXT_PUBLIC_API_URL_BASE directly so if the env var is undefined the
destination becomes "undefined/..." and routing breaks; validate the env var at
config evaluation (throw a clear error or provide a safe default) and construct
destinations only after confirming it's present (e.g., read
NEXT_PUBLIC_API_URL_BASE into a local constant, assert it's a non-empty string
and optionally ensure it has no trailing slash before appending '/api/v1/:path*'
and '/api/v2/:path*'), failing fast with a descriptive message if missing.
- `next/headers` 동적 임포트를 활용하여 SSR(서버)과 CSR(브라우저) 환경 모두에서 동작
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/web/app/_lib/axiosInstance.ts (1)
30-55: 토큰 갱신 로직의 구조분해 오류 및 타입 불일치 (Critical).두 가지 critical/major 이슈가 있습니다:
Line 40의 구조분해 오류 (Critical):
getToken()은res.data(즉,{ data: { accessToken, ... } })를 반환하지만, 이 코드는 최상위 레벨의accessToken을 기대합니다.newAccessToken은undefined가 되어 Line 43-45의 Authorization 헤더 설정이 실패합니다.- 토큰 갱신이 제대로 작동하지 않습니다.
Line 32의 타입 선언 불일치 (Major):
- Success handler는
response.data를 반환 (unwrapped)- Error handler는
Promise<AxiosResponse | AxiosError>타입- 실제 반환 타입과 일치하지 않아 TypeScript 타입 추론이 잘못됩니다.
해결 방법:
먼저
apps/web/app/_apis/services/login.ts의getToken함수를 수정하여 평탄화된 객체를 반환하도록 하세요 (해당 파일의 리뷰 코멘트 참조):// In apps/web/app/_apis/services/login.ts - return res.data + return data.data그런 다음, Line 32의 타입을 수정하세요:
axiosInstance.interceptors.response.use( (response: AxiosResponse) => response.data, - async (error: AxiosError): Promise<AxiosResponse | AxiosError> => { + async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }apps/web/app/_apis/services/login.ts (1)
8-22: 에러 처리 개선 및 주석 처리된 코드 정리가 필요합니다.
- Line 20: 에러 발생 시 콘솔에만 로그를 남기고 사용자에게 알리지 않습니다. 사용자에게 명확한 피드백을 제공해야 합니다.
- Line 10: 주석 처리된 iOS 감지 코드는 제거하거나 구현을 완료해야 합니다.
다음과 같이 개선하세요:
} catch (error) { - console.log(error) + console.error('Kakao 로그인 실패:', error) + alert('로그인에 실패했습니다. 다시 시도해주세요.') + throw error }주석 처리된 iOS 코드를 제거하세요:
const isAndroid = /Android/.test(navigator.userAgent) - // const isiOS = /(iPhone|iPad|iPod)/.test(navigator.userAgent);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/web/app/_apis/services/login.ts(1 hunks)apps/web/app/_lib/axiosInstance.ts(1 hunks)apps/web/app/_utils/getCookie/getCookie.ts(1 hunks)apps/web/app/_utils/getCookie/index.ts(1 hunks)apps/web/app/layout.tsx(2 hunks)apps/web/app/login/loading/success/page.tsx(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/web/app/_utils/getCookie/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/app/login/loading/success/page.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/app/_utils/getCookie/getCookie.ts (1)
apps/web/app/_utils/getCookie/index.ts (1)
getCookie(1-1)
apps/web/app/_lib/axiosInstance.ts (2)
apps/web/app/_utils/getCookie/getCookie.ts (1)
getCookie(25-35)apps/web/app/_apis/services/login.ts (1)
getToken(24-60)
🔇 Additional comments (3)
apps/web/app/_utils/getCookie/getCookie.ts (1)
1-35: 하이브리드 쿠키 유틸리티 구현이 올바릅니다.서버 환경에서는 Next.js의
next/headers를 동적으로 임포트하여 쿠키를 조회하고, 클라이언트 환경에서는cookies-next를 사용하는 접근 방식이 Next.js App Router의 권장 패턴과 일치합니다. 인터페이스 통일을 위한 Promise 반환도 적절합니다.apps/web/app/layout.tsx (1)
64-69: Kakao SDK 스크립트 로딩이 올바르게 수정되었습니다.이전 리뷰에서 지적된 critical 이슈(body 외부의 script 태그)가 해결되었습니다.
next/script를 사용하여 body 내부에 배치하였고,lazyOnload전략은 카카오 SDK와 같은 비필수 3rd party 스크립트에 적합합니다. integrity와 crossOrigin 속성도 보안 관점에서 올바릅니다.apps/web/app/_lib/axiosInstance.ts (1)
17-28: 요청 인터셉터가 올바르게 구현되었습니다.
getCookie유틸리티가 서버/클라이언트 환경을 모두 지원하도록 올바르게 구현되었으므로, 이 요청 인터셉터는 의도한 대로 동작합니다.
| export const getToken = async (): Promise<{ | ||
| tokenType: string | ||
| accessToken: string | ||
| accessTokenExpiresIn: number | ||
| }> => { | ||
| try { | ||
| const res = await axios.post( | ||
| process.env.NEXT_PUBLIC_API_URL + API_PATH.AUTH.TOKEN, | ||
| {}, | ||
| { | ||
| withCredentials: true, | ||
| }, | ||
| ) | ||
|
|
||
| const { data } = res | ||
| const { accessToken, accessTokenExpiresIn } = data.data | ||
| const expireDate = new Date(Date.now() + accessTokenExpiresIn) | ||
|
|
||
| setCookie('accessToken', accessToken, { | ||
| expires: expireDate, | ||
| secure: process.env.NODE_ENV === 'production', | ||
| sameSite: 'lax', | ||
| path: '/', | ||
| }) | ||
|
|
||
| return res.data | ||
| } catch (error) { | ||
| console.error('토큰 재발급 실패(세션 만료):', error) | ||
| deleteCookie('accessToken') | ||
|
|
||
| if (typeof window !== 'undefined') { | ||
| window.location.href = CLIENT_PATH.LOGIN | ||
| } | ||
|
|
||
| throw error | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getToken 반환 타입과 실제 반환값이 일치하지 않습니다 (Critical).
Line 24-28의 타입 선언은 평탄화된 객체 { tokenType, accessToken, accessTokenExpiresIn }를 반환한다고 명시하지만, Line 49는 res.data를 그대로 반환합니다. Line 39에서 data.data로 중첩된 구조를 사용하는 것을 보면 실제 반환값은 { data: { accessToken, ... } } 형태입니다.
이로 인해 apps/web/app/_lib/axiosInstance.ts의 Line 40에서 const { accessToken: newAccessToken } = await getToken()로 구조분해할 때 newAccessToken이 undefined가 되어 토큰 갱신이 실패합니다.
다음 중 하나를 적용하여 수정하세요:
방법 1: 반환 타입과 일치하도록 평탄화된 객체 반환 (권장)
setCookie('accessToken', accessToken, {
expires: expireDate,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})
- return res.data
+ return data.data
} catch (error) {방법 2: 타입 선언을 실제 반환값과 일치시키기
-export const getToken = async (): Promise<{
- tokenType: string
- accessToken: string
- accessTokenExpiresIn: number
-}> => {
+export const getToken = async (): Promise<{
+ data: {
+ tokenType: string
+ accessToken: string
+ accessTokenExpiresIn: number
+ }
+}> => {그리고 axiosInstance.ts의 사용처도 수정:
- const { accessToken: newAccessToken } = await getToken()
+ const { data: { accessToken: newAccessToken } } = await getToken()방법 1을 권장합니다.
#️⃣연관된 이슈
📝작업 내용
1. 카카오 소셜 로그인 프로세스 구현
AccessToken: Response Body로 수신 → 직접 브라우저 쿠키에 저장 (클라이언트 및 Next 서버에서 접근 가능하도록 설정)
RefreshToken: 백엔드로부터 HttpOnly 쿠키로 수신
2.
Middleware설정인증 보호: 미인증 사용자(토큰 부재)가 보호된 라우트에 접근 시 로그인 페이지로 강제 리다이렉트 처리
Matcher 적용: 불필요한 리소스 낭비를 막기 위해 인증 검사가 필요한 경로에만 미들웨어가 동작하도록 범위 제한
3.
Rewrites도입 배경백엔드 API 서버와 프론트엔드(Localhost)의 도메인이 서로 달라 발생하는 쿠키 저장(Set-Cookie) 문제를 해결하기 위해 Next.js Rewrites를 도입했습니다.
1. 문제 상황 (Domain Mismatch)
백엔드 도메인과 Localhost가 달라 브라우저가 보안상
Set-Cookie를 거부하는 문제 발생SameSite: None; Secure설정이 필요하지만, Localhost는 HTTPS가 아니어서 적용 불가2. 해결 방법: Proxy (Rewrites) 적용
브라우저는
localhost/api/...로 요청을 보내지만, Next.js가 내부적으로 이를 백엔드 실제 주소로 프록시(Proxy) 처리효과:
브라우저는 자신이
localhost와 통신한다고 판단하여 쿠키를 정상적으로 저장 및 전송백엔드가 설정한
HttpOnly쿠키(RefreshToken)가 도메인 문제없이 브라우저에 저장됨이후 요청 시 브라우저가
localhost도메인으로 쿠키를 실어 보내면, Rewrites가 이를 백엔드로 그대로 전달하여 인증 유지 가능3. Route Handler 대신 Rewrites를 선택한 이유
Route Handler(route.ts)에서 쿠키를 받아 재설정하는 방식을 고려했으나, 이 경우 쿠키의 도메인이 localhost로 덮어씌워짐이로 인해 백엔드 서버가 추후 요청에서
RefreshToken을 인식하지 못하거나, 도메인 불일치로 인해 쿠키 공유가 복잡해지는 사이드 이펙트 방지'+ 이후 배포 후 도메인 일치 한다면 route 방식 도입 고려
'+ 현재 login/loading/success uri 변경 필요
스크린샷 (선택)
💬리뷰 요구사항(선택)
Summary by CodeRabbit
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.