Skip to content

[기능 구현] 카카오 OAuth 리다이렉트 페이지 및 인증 API 연동 구현#45

Merged
Dobbymin merged 9 commits intomainfrom
feat#42-kakao-login
Aug 21, 2025
Merged

[기능 구현] 카카오 OAuth 리다이렉트 페이지 및 인증 API 연동 구현#45
Dobbymin merged 9 commits intomainfrom
feat#42-kakao-login

Conversation

@Dobbymin
Copy link
Copy Markdown
Contributor

📝 요약 (Summary)

카카오 OAuth 인증 플로우를 완성하기 위해 리다이렉트 페이지와 인증 API 연동 기능을 구현했습니다. 사용자가 카카오 로그인 후 안전하게 인증을 처리하고 메인 페이지로 이동할 수 있도록 했습니다.

✅ 주요 변경 사항 (Key Changes)

  • OAuth 리다이렉트 페이지 생성 및 라우터 설정
  • 인증 티켓 검증을 위한 API 모듈 및 훅 구현
  • 카카오 로그인 버튼에 리다이렉트 URL 설정 기능 추가
  • React Query를 활용한 안전한 API 호출 및 상태 관리
  • OAuth 에러 처리 및 사용자 피드백 개선

💻 상세 구현 내용 (Implementation Details)

OAuth 리다이렉트 페이지 구현

  • 경로: /oauth/redirect - OAuth 인증 후 리다이렉트되는 전용 페이지
  • URL 파라미터 처리: ticket, error, error_description 파라미터 추출 및 검증
  • 조건부 API 호출: ticket이 있고 OAuth 에러가 없을 때만 인증 API 호출
  • 로딩 상태 관리: API 호출 중 스피너 표시로 사용자 경험 개선

인증 API 모듈 구현

  • get-ticket.api.ts: 백엔드 API와 통신하여 티켓 검증
  • getAuthTicket.ts: React Query를 활용한 인증 훅 구현
  • 타입 안정성: TypeScript를 활용한 응답 데이터 타입 정의
  • 에러 처리: API 호출 실패 시 적절한 에러 메시지 표시

카카오 로그인 버튼 개선

  • 리다이렉트 URL 설정: OAuth 서버에 명시적으로 리다이렉트 URL 전달
  • URL 인코딩: 안전한 URL 전달을 위한 인코딩 처리
  • BASE_URL 활용: 환경에 따른 서버 URL 동적 설정

라우터 설정

  • 새로운 라우트: /oauth/redirect 경로를 Auth 라우트에 추가
  • 레이아웃 적용: 인증 관련 페이지에 적합한 Auth 레이아웃 적용
  • 라우터 경로 상수: ROUTER_PATH.OAUTH_REDIRECT 추가

🚀 트러블 슈팅 (Trouble Shooting)

  • React Query 조건부 호출: ticket이 없을 때 불필요한 API 호출 방지를 위한 조건부 로직 구현
  • OAuth 에러 처리: 다양한 OAuth 에러 상황에 대한 적절한 처리 로직 구현
  • 타입 안정성: TypeScript 컴파일 에러 해결을 위한 export 구조 개선
  • 상태 관리: 여러 useEffect를 활용한 복잡한 상태 관리 로직 구현

⚠️ 알려진 이슈 및 참고 사항 (Known Issues & Notes)

  • 백엔드 API 엔드포인트가 /api/auth/ticket로 설정되어 있으므로, 실제 서버와 일치하는지 확인 필요
  • refreshToken을 localStorage에 저장하고 있으므로, 보안 요구사항에 따라 세션스토리지나 쿠키 사용 고려
  • OAuth 서버에서 리다이렉트 URL을 올바르게 처리하는지 백엔드 팀과 협의 필요
  • 에러 상황에서 사용자에게 더 구체적인 안내 메시지 제공 고려

📸 스크린샷 (Screenshots)

[OAuth 리다이렉트 페이지 스크린샷을 여기에 추가해주세요]

#️⃣ 관련 이슈 (Related Issues)

@Dobbymin Dobbymin linked an issue Aug 21, 2025 that may be closed by this pull request
@Dobbymin Dobbymin self-assigned this Aug 21, 2025
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @Dobbymin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 카카오 OAuth 인증 플로우를 프론트엔드에서 완전히 구현하여 사용자가 카카오 로그인을 통해 안전하게 인증을 처리하고 애플리케이션의 메인 페이지로 이동할 수 있도록 합니다. 이는 OAuth 리다이렉트 페이지, 인증 API 연동, 그리고 관련 라우팅 및 에러 처리를 포함합니다.

Highlights

  • OAuth 리다이렉트 페이지 구현: 카카오 인증 후 /oauth/redirect 경로로 리다이렉트되는 전용 페이지를 생성하고, URL 파라미터(ticket, error)를 처리하여 인증 티켓을 검증하거나 OAuth 에러를 표시합니다.
  • 인증 API 모듈 및 훅 개발: 백엔드 /api/auth/ticket 엔드포인트와 통신하여 인증 티켓을 검증하는 API 모듈(get-ticket.api.ts)과 React Query를 활용한 useGetAuthTicket 훅을 구현하여 안전하고 효율적인 데이터 페칭을 가능하게 합니다.
  • 카카오 로그인 버튼 개선: 카카오 로그인 버튼 클릭 시 OAuth 서버로 명시적인 리다이렉트 URL을 전달하도록 수정하여, 인증 플로우의 안정성을 높였습니다.
  • 라우팅 및 상태 관리: 새로운 /oauth/redirect 라우트를 추가하고, React Query를 이용한 로딩 및 에러 상태 관리, 그리고 localStoragerefreshToken을 저장하는 로직을 포함합니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이 PR은 카카오 OAuth 인증 흐름을 구현하기 위한 리다이렉트 페이지와 API 연동을 추가합니다. 전반적으로 기능 구현은 잘 이루어졌지만, 코드의 유지보수성, 보안, 그리고 React Query의 효율적인 사용 측면에서 몇 가지 개선점을 제안합니다. 특히, localStorage에 토큰을 저장하는 방식은 보안에 취약할 수 있으므로 httpOnly 쿠키 사용을 강력히 권장합니다. 또한, 여러 useEffect 훅을 통합하고 React Query의 enabled 옵션을 활용하여 코드를 더 깔끔하고 효율적으로 만들 수 있습니다.


export const fetchInstance = initInstance({
baseURL: BASE_URL,
withCredentials: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

withCredentials: true 옵션이 제거되었습니다. 만약 애플리케이션의 다른 부분에서 인증을 위해 쿠키를 사용하고 있다면, 이 변경으로 인해 인증이 제대로 동작하지 않을 수 있습니다. 이 변경이 의도된 것인지, 그리고 다른 기능에 미치는 영향이 없는지 확인이 필요합니다. 토큰 기반 인증으로 완전히 전환하는 과정이라면 괜찮지만, 중요한 변경이므로 명확히 하는 것이 좋습니다.

console.log('OAuth 인증 성공!', authData);

// refreshToken을 localStorage에 저장
localStorage.setItem('refreshToken', authData.refreshToken);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

refreshTokenlocalStorage에 저장하는 것은 XSS(Cross-Site Scripting) 공격에 취약할 수 있습니다. 악의적인 스크립트가 localStorage에 접근하여 토큰을 탈취할 수 있기 때문입니다. 보안을 강화하기 위해 httpOnly 속성을 가진 쿠키에 토큰을 저장하는 것을 강력히 권장합니다. httpOnly 쿠키는 JavaScript로 접근할 수 없어 XSS 공격으로부터 토큰을 보호할 수 있습니다.

export const KakaoLoginButton = () => {
const kakaoLogin = () => {
// OAuth 서버로 리다이렉트하되, 리다이렉트 URL을 명시적으로 지정
const redirectUri = encodeURIComponent(`${window.location.origin}/oauth/redirect`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

리다이렉트 URL 경로가 하드코딩되어 있습니다. ROUTER_PATH 상수를 사용하면 코드의 일관성과 유지보수성을 높일 수 있습니다. import { ROUTER_PATH } from '@/shared'를 추가하고 ROUTER_PATH.OAUTH_REDIRECT를 사용하세요.

Suggested change
const redirectUri = encodeURIComponent(`${window.location.origin}/oauth/redirect`);
const redirectUri = encodeURIComponent(`${window.location.origin}${ROUTER_PATH.OAUTH_REDIRECT}`);


export const GET_TICKET_API_PATH = '/api/auth/ticket';

interface GetTicketApiResponse {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

GetTicketApiResponse 타입을 export하여 다른 파일(특히 custom hook)에서 재사용할 수 있도록 하는 것이 좋습니다. 이렇게 하면 타입 안정성을 높일 수 있습니다.

Suggested change
interface GetTicketApiResponse {
export interface GetTicketApiResponse {

Comment on lines +9 to +14
export const useGetAuthTicket = (ticket: string) => {
return useQuery({
queryKey: GetAuthTicketQueryKey.ticket(ticket),
queryFn: () => getTicketApi(ticket),
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useQueryenabled 옵션을 사용하여 조건부로 쿼리를 실행할 수 있도록 useGetAuthTicket 훅을 수정하는 것을 권장합니다. 이렇게 하면 불필요한 API 호출을 방지하고 코드를 더 명확하게 만들 수 있습니다. useQuery의 옵션을 받을 수 있도록 훅의 인자를 확장해주세요. 이 변경은 get-ticket.api.ts에서 GetTicketApiResponse 타입을 export한 후에 적용해야 합니다.

Suggested change
export const useGetAuthTicket = (ticket: string) => {
return useQuery({
queryKey: GetAuthTicketQueryKey.ticket(ticket),
queryFn: () => getTicketApi(ticket),
});
};
export const useGetAuthTicket = (
ticket: string,
options?: Omit<UseQueryOptions<GetTicketApiResponse>, 'queryKey' | 'queryFn'>,
) => {
return useQuery({
queryKey: GetAuthTicketQueryKey.ticket(ticket),
queryFn: () => getTicketApi(ticket),
...options,
});
};

Comment on lines +19 to +30
useEffect(() => {
if (oauthError) {
setError(errorDescription || `OAuth 인증 오류: ${oauthError}`);
}
}, [oauthError, errorDescription]);

// ticket이 없으면 에러 처리
useEffect(() => {
if (!oauthError && !ticket) {
setError('인증 토큰이 없습니다. 다시 로그인해주세요.');
}
}, [ticket, oauthError]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

두 개의 useEffect 훅이 모두 URL 파라미터를 검증하고 에러 상태를 설정하는 역할을 합니다. 이 로직을 하나의 useEffect로 통합하면 코드가 더 간결해지고, 상태 업데이트 로직을 한 곳에서 관리할 수 있어 잠재적인 race condition을 방지할 수 있습니다.

Suggested change
useEffect(() => {
if (oauthError) {
setError(errorDescription || `OAuth 인증 오류: ${oauthError}`);
}
}, [oauthError, errorDescription]);
// ticket이 없으면 에러 처리
useEffect(() => {
if (!oauthError && !ticket) {
setError('인증 토큰이 없습니다. 다시 로그인해주세요.');
}
}, [ticket, oauthError]);
useEffect(() => {
if (oauthError) {
setError(errorDescription || `OAuth 인증 오류: ${oauthError}`);
} else if (!ticket) {
setError('인증 토큰이 없습니다. 다시 로그인해주세요.');
}
}, [oauthError, errorDescription, ticket]);

Comment on lines +33 to +38
const shouldCallApi = !!ticket && !oauthError;
const {
data: authData,
error: apiError,
isLoading,
} = useGetAuthTicket(shouldCallApi ? ticket : '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useGetAuthTicket 훅을 호출할 때 삼항 연산자를 사용하여 빈 문자열을 전달하는 대신, React Query의 enabled 옵션을 사용하는 것이 더 효율적이고 명확한 방법입니다. 이렇게 하면 조건이 충족되지 않았을 때 불필요한 API 요청이 발생하는 것을 막을 수 있습니다. 이 변경을 적용하려면 useGetAuthTicket 훅의 수정이 필요합니다.

  const { data: authData, error: apiError, isLoading } = useGetAuthTicket(ticket || '', {
    enabled: !!ticket && !oauthError,
  });

// API 에러 처리
useEffect(() => {
if (apiError) {
console.error('API 호출 중 오류:', apiError);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

console.error는 개발 중에는 유용하지만, 프로덕션 코드에는 적절한 로깅 라이브러리(예: Sentry, LogRocket)를 사용한 에러 리포팅으로 대체하는 것이 좋습니다. 실제 사용자에게서 발생하는 에러를 추적하고 디버깅하는 데 도움이 됩니다.

// 성공 시 처리
useEffect(() => {
if (authData && !isLoading && !apiError && shouldCallApi) {
console.log('OAuth 인증 성공!', authData);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

console.log는 개발 단계에서 디버깅 목적으로 유용하지만, 프로덕션 빌드에서는 제거하는 것이 좋습니다. 불필요한 정보가 브라우저 콘솔에 노출되는 것을 방지할 수 있습니다.

Comment on lines +49 to +59
useEffect(() => {
if (authData && !isLoading && !apiError && shouldCallApi) {
console.log('OAuth 인증 성공!', authData);

// refreshToken을 localStorage에 저장
localStorage.setItem('refreshToken', authData.refreshToken);

// 메인 페이지로 이동
navigate(ROUTER_PATH.MAIN, { replace: true });
}
}, [authData, isLoading, apiError, navigate, shouldCallApi]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

성공 처리 useEffect의 조건문(if (authData && !isLoading && !apiError && shouldCallApi))이 다소 복잡하고, 의존성 배열에 불필요한 항목이 포함되어 있습니다. useQuerydata가 존재하면 isLoadingfalse이고 errornull인 상태이므로, if (authData)로 조건을 단순화할 수 있습니다. 의존성 배열도 [authData, navigate]로 줄일 수 있습니다.

  // 성공 시 처리
  useEffect(() => {
    if (authData) {
      console.log('OAuth 인증 성공!', authData);

      // refreshToken을 localStorage에 저장
      localStorage.setItem('refreshToken', authData.refreshToken);

      // 메인 페이지로 이동
      navigate(ROUTER_PATH.MAIN, { replace: true });
    }
  }, [authData, navigate]);

@Dobbymin Dobbymin merged commit 7e9ff5c into main Aug 21, 2025
2 checks passed
@Dobbymin Dobbymin deleted the feat#42-kakao-login branch August 21, 2025 21:35
@Dobbymin Dobbymin restored the feat#42-kakao-login branch August 21, 2025 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[기능 구현] 카카오 로그인 기능 구현

1 participant