728x90
반응형

안녕하세요! 오늘은 React 애플리케이션에서 카카오 소셜 로그인을 구현하는 방법에 대해 공유하려고 합니다. 프로젝트에서 카카오 로그인을 적용하면서 경험한 내용을 바탕으로 작성했습니다.

목차

  1. 준비 사항
  2. 카카오 개발자 계정 및 앱 설정
  3. 프로젝트에 카카오 SDK 추가하기
  4. 카카오 로그인 버튼 컴포넌트 구현
  5. 인증 코드 처리를 위한 콜백 페이지 구현
  6. 라우팅 설정
  7. 백엔드 연동
  8. 발생 가능한 문제 및 해결 방법
  9. 마치며

1. 준비 사항

카카오 소셜 로그인을 구현하기 위해 다음과 같은 기술 스택을 사용했습니다.

  • React
  • TypeScript
  • Vite
  • React Router
  • Axios

2. 카카오 개발자 계정 및 앱 설정

먼저, 카카오 개발자 사이트에 접속하여 계정을 생성하고 앱을 등록합니다.

앱 등록 및 설정 단계:

 

  1. 카카오 개발자 사이트에 로그인하고 "내 애플리케이션" 메뉴에서 애플리케이션 추가
  2. 앱 이름, 회사명 등 기본 정보 입력
  3. 플랫폼 설정에서 "Web" 플랫폼 추가
    • 사이트 도메인 등록: http://localhost:5173과 프로덕션 도메인
  4. 카카오 로그인 활성화
    • 동의항목 설정: 필수로 닉네임(profile_nickname)
    • 활성화 설정에서 상태를 "ON"으로 변경
  5. 카카오 로그인 Redirect URI 등록
    • http://localhost:5173/oauth/kakao/callback (개발 환경)
    • https://your-production-domain.com/oauth/kakao/callback (프로덕션 환경)
  6. 앱 키 정보 확인
    • JavaScript 키를 복사해서 프로젝트 환경 변수에 설정

3. 프로젝트에 카카오 SDK 추가하기

HTML에 카카오 SDK 스크립트 추가

index.html 파일의 <head> 섹션에 카카오 SDK 스크립트를 추가합니다:

<script src="https://t1.kakaocdn.net/kakao_js_sdk/2.4.0/kakao.min.js" 
        integrity="sha384-mXVrIX2T/Kszp6Z0aEWaA8Nm7J6/ZeWXbL8UpGRjKwWe56Srd/iyNmWMBhcItAjH" 
        crossorigin="anonymous"></script>

환경 변수 설정

프로젝트 루트 디렉토리에 .env 파일을 생성하고 카카오 JavaScript 키를 추가합니다.

VITE_KAKAO_JAVASCRIPT_KEY='your_javascript_key_here'

TypeScript 타입 정의

카카오 SDK의 타입을 정의하여 TypeScript에서 사용할 수 있도록 합니다.

// src/types/kakao.ts
export interface KakaoAuthResponse {
  access_token: string;
  token_type: string;
  refresh_token: string;
  expires_in: number;
  scope: string;
}

export interface KakaoSDK {
  init: (key: string) => void;
  isInitialized: () => boolean;
  Auth: {
    login: (options: {
      success: (response: KakaoAuthResponse) => void;
      fail: (error: Error) => void;
    }) => void;
    authorize: (options: {
      redirectUri: string;
      scope?: string;
      state?: string;
    }) => void;
  };
}

declare global {
  interface Window {
    Kakao: KakaoSDK;
  }
}

4. 카카오 로그인 버튼 컴포넌트 구현

소셜 로그인 버튼 목록 컴포넌트

// src/components/common/List/SocialLoginList.tsx
import styles from './SocialLoginList.module.scss';
import SocialLoginListItem from '../ListItem/SocialLoginListItem';
import KakaoIcon from '../../../assets/social/kakao.svg';
import GoogleIcon from '../../../assets/social/google.svg';
import NaverIcon from '../../../assets/social/naver.svg';
import { useEffect } from 'react';
import { useAlertStore } from '../../../stores/alertStore';

const KakaoLoginIcon = () => {
  return <img src={KakaoIcon} alt="카카오 로그인 하기" />;
};

// 다른 소셜 로그인 아이콘 컴포넌트...

const KAKAO_LOGIN_LABEL = '카카오 로그인 하기';

const socialAccounts = [
  { id: 1, text: <KakaoLoginIcon />, ariaLabel: KAKAO_LOGIN_LABEL },
  { id: 2, text: <GoogleLoginIcon />, ariaLabel: '구글 로그인 하기' },
  { id: 3, text: <NaverLoginIcon />, ariaLabel: '네이버 로그인 하기' },
];

const SocialLoginList: React.FC = () => {
  const { setTitle, setContent, openAlert } = useAlertStore();

  const handleSocialLogin = (provider: string) => {
    if (provider === KAKAO_LOGIN_LABEL) {
      // 카카오 로그인 처리
      if (window.Kakao && window.Kakao.Auth) {
        try {
          // 인가 코드 방식으로 로그인 구현
          const REDIRECT_URI = window.location.origin + '/oauth/kakao/callback';
          window.Kakao.Auth.authorize({
            redirectUri: REDIRECT_URI,
            scope: 'profile_nickname',
          });
          // authorize는 리다이렉트 방식이므로 아래 코드는 실행되지 않음
        } catch (error: unknown) {
          console.error('카카오 로그인 처리 중 오류:', error);
          setTitle('오류');
          setContent('카카오 로그인 처리 중 오류가 발생했습니다.');
          openAlert();
        }
      } else {
        console.error('카카오 SDK가 초기화되지 않았습니다.');
        setTitle('오류');
        setContent('카카오 로그인을 위한 준비가 되지 않았습니다. 잠시 후 다시 시도해주세요.');
        openAlert();
      }
    } else {
      // 다른 소셜 로그인은 개발 중 메시지 표시
      setTitle('안내');
      setContent(`${provider.replace(' 하기', '')} 기능은 현재 개발 중입니다.<br />빠른 시일 내에 서비스 제공 예정입니다.`);
      openAlert();
    }
  };

  // Kakao SDK 초기화
  useEffect(() => {
    const hasKakaoLogin = socialAccounts.some(
      (account) => account.ariaLabel === KAKAO_LOGIN_LABEL
    );

    if (hasKakaoLogin && window.Kakao) {
      const appKey = import.meta.env.VITE_KAKAO_JAVASCRIPT_KEY;

      if (!appKey) {
        console.error('카카오 앱 키가 설정되지 않았습니다. 환경 변수를 확인하세요.');
        return;
      }

      try {
        if (!window.Kakao.isInitialized()) {
          window.Kakao.init(appKey);
          console.log('카카오 SDK 초기화 성공:', window.Kakao.isInitialized());
        } else {
          console.log('카카오 SDK는 이미 초기화되었습니다.');
        }
      } catch (error) {
        console.error('카카오 SDK 초기화 중 오류:', error);
      }
    }
  }, []);

  // 렌더링 부분
  return (
    <>
      <ul
        className={styles['social-login-list']}
        aria-label="소셜 로그인 버튼 목록"
      >
        {socialAccounts.map((account) => (
          <SocialLoginListItem key={account.id}>
            <button
              className={styles['social-login-button']}
              aria-label={account.ariaLabel}
              onClick={() => handleSocialLogin(account.ariaLabel)}
            >
              {account.text}
            </button>
          </SocialLoginListItem>
        ))}
      </ul>
    </>
  );
};

export default SocialLoginList;

5. 인증 코드 처리를 위한 콜백 페이지 구현

카카오 로그인 과정에서 인증 코드를 받아 처리하는 콜백 페이지입니다.

// src/pages/OAuth/KakaoCallback.tsx
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { useAlertStore } from '../../stores/alertStore';

const KakaoCallback = () => {
  const location = useLocation();
  const navigate = useNavigate();
  const { setTitle, setContent, openAlert } = useAlertStore();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const getKakaoToken = async () => {
      // URL에서 인가 코드 추출
      const code = location.search.split('=')[1];

      if (!code) {
        setTitle('오류');
        setContent('인증 코드를 찾을 수 없습니다.');
        openAlert();
        navigate('/login');
        return;
      }

      try {
        const CLIENT_ID = import.meta.env.VITE_KAKAO_JAVASCRIPT_KEY;
        const REDIRECT_URI = window.location.origin + '/oauth/kakao/callback';

        // 1. 카카오 API로 토큰 요청
        const tokenResponse = await axios.post(
          'https://kauth.kakao.com/oauth/token',
          null,
          {
            params: {
              grant_type: 'authorization_code',
              client_id: CLIENT_ID,
              redirect_uri: REDIRECT_URI,
              code
            },
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
            }
          }
        );

        if (tokenResponse.data.access_token) {
          // 2. 백엔드에 카카오 토큰 전송
          try {
            const backendResponse = await axios.post('/api/auth/kakao', {
              token: tokenResponse.data.access_token
            });

            if (backendResponse.data.success) {
              // 로그인 성공 처리
              setTitle('로그인 성공');
              setContent('카카오 계정으로 로그인되었습니다.');
              openAlert();
              navigate('/');
            } else {
              throw new Error('백엔드 인증 실패');
            }
          } catch (error) {
            console.error('백엔드 인증 오류:', error);
            setTitle('로그인 실패');
            setContent('서버 인증 과정에서 오류가 발생했습니다.');
            openAlert();
            navigate('/login');
          }
        } else {
          throw new Error('카카오 토큰 발급 실패');
        }
      } catch (error) {
        console.error('카카오 인증 오류:', error);
        setTitle('로그인 실패');
        setContent('카카오 인증 과정에서 오류가 발생했습니다.');
        openAlert();
        navigate('/login');
      } finally {
        setLoading(false);
      }
    };

    getKakaoToken();
  }, [location, navigate, openAlert, setContent, setTitle]);

  // 로딩 상태 표시
  if (loading) {
    return (
      <div style={{ 
        display: 'flex', 
        justifyContent: 'center', 
        alignItems: 'center', 
        height: '100vh',
        flexDirection: 'column',
        gap: '16px'
      }}>
        <div className="spinner"></div>
        <p>카카오 로그인 처리 중...</p>
      </div>
    );
  }

  return null;
};

export default KakaoCallback;

6. 라우팅 설정

React Router를 사용하여 콜백 경로를 설정합니다.

// src/routes/AppRoutes.tsx
import { Routes, Route } from 'react-router-dom';
import { Suspense } from 'react';
// 다른 import 문...
import KakaoCallback from '../pages/OAuth/KakaoCallback';

const AppRoutes = () => {
  return (
    <Suspense fallback={<div>로딩 중...</div>}>
      <Routes>
        {/* OAuth 콜백 라우트 */}
        <Route path="/oauth/kakao/callback" element={<KakaoCallback />} />

        {/* 다른 라우트들... */}
      </Routes>
    </Suspense>
  );
};

export default AppRoutes;

7. 백엔드 연동

카카오 로그인의 전체 플로우는 다음과 같습니다.

  1. 사용자가 카카오 로그인 버튼 클릭
  2. 카카오 인증 페이지로 리다이렉트
  3. 카카오 로그인 완료 후 콜백 URL로 리다이렉트 (인가 코드 포함)
  4. 인가 코드로 카카오 API에서 액세스 토큰 요청
  5. 발급받은 토큰을 백엔드 서버에 전송
  6. 백엔드에서 사용자 정보 조회 및 인증 처리
  7. 인증 성공 시 서비스 토큰 발급 또는 로그인 완료 처리

백엔드에서는 다음과 같은 처리가 필요합니다.

  1. 카카오 액세스 토큰 검증
  2. 해당 토큰으로 카카오 API를 호출하여 사용자 정보 조회
  3. 자체 회원 DB와 매핑 (기존 회원 인증 또는 신규 회원 가입)
  4. JWT 등의 토큰 발급 및 사용자 인증 처리

8. 발생 가능한 문제 및 해결 방법

1. SDK 초기화 문제

문제: Kakao.Auth.login is not a function 에러가 발생하는 경우

해결:

  • 최신 카카오 SDK에서는 login() 대신 authorize() 메서드 사용
  • 타입 정의 파일에 authorize 메서드 추가
  • SDK 스크립트가 제대로 로드되었는지 확인

2. 리다이렉트 URI 불일치

문제: 카카오에서 "잘못된 리다이렉트 URI" 에러가 발생하는 경우

해결:

  • 카카오 개발자 콘솔에 등록된 리다이렉트 URI와 코드의 URI가 정확히 일치하는지 확인
  • 프로토콜(http/https), 포트 번호, 경로가 모두 동일해야 함

3. 권한 설정 문제

문제: 필요한 사용자 정보에 접근할 수 없는 경우

해결:

  • 카카오 개발자 콘솔에서 동의항목 설정 확인
  • 필요한 scope 파라미터 설정 (예: profile_nickname)

4. CORS 이슈

문제: 백엔드 API 호출 시 CORS 에러가 발생하는 경우

해결:

  • 백엔드 서버의 CORS 설정 확인
  • 프론트엔드에서 API 호출 시 적절한 헤더 설정
  • 개발 환경에서는 프록시 설정 고려

9. 마치며

이상으로 React 앱에서 카카오 소셜 로그인을 구현하는 방법에 대해 알아보았습니다. 카카오 로그인은 사용자에게 편리한 로그인 방법을 제공하면서도, 개발자에게는 복잡한 인증 로직을 간소화할 수 있는 좋은 방법입니다.

실제 구현 시에는 사용자 경험과 보안을 모두 고려해야 하며, 프로덕션 환경에서는 더 철저한 에러 처리와 예외 상황 대응이 필요합니다.

이 글이 여러분의 카카오 소셜 로그인 구현에 도움이 되었으면 좋겠습니다!

참고 자료

728x90
반응형
vanillinav