개발 단계 (Development Stage)/개발 (Development)

[사이드프로젝트] 카카오 소셜 로그인 아키텍처의 진화: 프론트엔드 주도 방식에서 백엔드 위임 방식으로

vanillinav 2025. 4. 14. 00:01
728x90
반응형

1. 서론

안녕하세요. 소셜 로그인은 현대 웹 애플리케이션의 필수 요소가 되었습니다. 특히 국내에서는 카카오 로그인이 사용자 편의성을 크게 높이는 핵심 기능입니다. 이 글에서는 카카오 소셜 로그인 구현 아키텍처의 진화를 자세히 살펴보고, 각 방식의 장단점을 분석하겠습니다. 코드 예시와 흐름도를 통해 두 방식의 차이점을 명확히 이해할 수 있도록 도와드리겠습니다.

2. 이전 방식: 프론트엔드 주도 카카오 로그인

프론트엔드 주도 방식은 인증 흐름의 대부분을 프론트엔드에서 처리하는 방식입니다. 이 방식에서는 카카오 SDK를 직접 초기화하고, 인증 코드 요청부터 토큰 교환까지 프론트엔드에서 담당합니다.

2.1 구현 흐름

2.2 코드 구현 예시

2.2.1 카카오 SDK 초기화

// SocialLoginList.tsx
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);
    }
  }
}, []);

2.2.2 인증 코드 요청

// SocialLoginList.tsx - handleSocialLogin 함수
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,profile_image',
        });
        // authorize는 리다이렉트 방식이므로 아래 코드는 실행되지 않음
      } catch (error) {
        console.error('카카오 로그인 처리 중 오류:', error);
        setTitle('오류');
        setContent('카카오 로그인 처리 중 오류가 발생했습니다.');
        openAlert();
      }
    } else {
      console.error('카카오 SDK가 초기화되지 않았습니다.');
      setTitle('오류');
      setContent('카카오 로그인을 위한 준비가 되지 않았습니다. 잠시 후 다시 시도해주세요.');
      openAlert();
    }
  } else {
    // 다른 소셜 로그인 처리
  }
};

2.2.3 액세스 토큰 획득 및 백엔드 인증

// KakaoCallback.tsx
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_REST_API_KEY;
      const REDIRECT_URI = window.location.origin + '/oauth/kakao/callback';

      // 1. 카카오 API로 토큰 요청
      const formData = new URLSearchParams();
      formData.append('grant_type', 'authorization_code');
      formData.append('client_id', CLIENT_ID);
      formData.append('redirect_uri', REDIRECT_URI);
      formData.append('code', code);

      const tokenResponse = await axios.post(
        'https://kauth.kakao.com/oauth/token',
        formData.toString(),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
          }
        }
      );

      if (tokenResponse.data.access_token) {
        // 2. 백엔드에 카카오 토큰 전송
        try {
          const apiBaseUrl = import.meta.env.VITE_API_URL || '';
          const apiPath = '/api/auth/kakao';

          const backendResponse = await axios.post(`${apiBaseUrl}${apiPath}`, {
            token: tokenResponse.data.access_token
          }, {
            withCredentials: true
          });

          if (backendResponse.data.success) {
            // 로그인 성공 처리
            setTitle('로그인 성공');
            setContent('카카오 계정으로 로그인되었습니다.');
            openAlert();
            navigate('/home');
          } else {
            throw new Error('백엔드 인증 실패');
          }
        } catch (error) {
          // 오류 처리
        }
      }
    } catch (error) {
      // 오류 처리
    }
  };

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

2.3 프론트엔드 주도 방식의 장단점

장점

  • 독립적인 개발: 백엔드 구현에 의존하지 않고 프론트엔드에서 독립적으로 로그인 기능 개발 가능
  • 상세한 제어: 인증 흐름의 각 단계를 상세하게 제어할 수 있음
  • 유연한 UI/UX: 로그인 과정에서 다양한 사용자 경험 요소 구현 가능

단점

  • 보안 취약성: 클라이언트에 중요한 API 키와 토큰이 노출됨
  • 코드 복잡성: 프론트엔드가 많은 인증 로직을 처리해야 함
  • CORS 이슈: 다양한 도메인 간 요청으로 CORS 문제 발생 가능

3. 새로운 방식: 백엔드 위임 카카오 로그인

백엔드 위임 방식은 인증 흐름의 핵심 부분을 백엔드 서버에 위임하는 방식입니다. 프론트엔드는 단순히 백엔드 엔드포인트로 리다이렉트하고, 인증 결과만 처리합니다.

3.1 구현 흐름

3.2 코드 구현 예시

3.2.1 백엔드 엔드포인트로 리다이렉트

// SocialLoginList.tsx - 단순화된 handleSocialLogin 함수
const handleSocialLogin = (provider: string) => {
  if (provider === KAKAO_LOGIN_LABEL) {
    try {
      const apiBaseUrl = import.meta.env.VITE_API_URL || '';
      const kakaoAuthUrl = `${apiBaseUrl}/oauth2/authorization/kakao`;

      console.log('카카오 로그인 페이지로 이동:', kakaoAuthUrl);
      window.location.href = kakaoAuthUrl;
    } catch (error: unknown) {
      console.error('카카오 로그인 처리 중 오류:', error);
      setTitle('오류');
      setContent('카카오 로그인 처리 중 오류가 발생했습니다.');
      openAlert();
    }
  } else {
    // 다른 소셜 로그인 처리
  }
};

3.2.2 리다이렉트 처리 및 토큰 교환

// OAuthRedirect.tsx
useEffect(() => {
  const processLogin = async () => {
    try {
      const params = new URLSearchParams(location.search);
      const status = params.get('status');
      const code = params.get('social-code');

      if (status === 'success' && code) {
        console.log('✅ 받은 uuid:', code);

        try {
          // 백엔드 API를 호출하여 실제 인증 정보를 교환
          const response = await axios.get(`https://danjitalk.duckdns.org/api/oauth/exchange?code=${code}`, {
            withCredentials: true, // 쿠키 전송 허용
          });

          console.log('🔄 토큰 교환 성공:', response.data);

          // 로컬 스토리지에 로그인 정보 저장
          localStorage.setItem('isLoggedIn', 'true');
          localStorage.setItem('socialCode', code);
          localStorage.setItem('loginType', 'kakao');
          localStorage.removeItem('prevPath');

          // 성공 메시지 표시
          setTitle('로그인 성공');
          setContent('소셜 계정으로 로그인되었습니다.');
          openAlert();

          // 홈 페이지로 리다이렉트
          setLoadingState('success');
          setTimeout(() => {
            navigate('/home', { replace: true });
          }, 800);
        } catch (exchangeError) {
          // 오류 처리
        }
      } else {
        // 오류 처리
      }
    } catch (error) {
      // 오류 처리
    }
  };

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

3.3 백엔드 위임 방식의 장단점

장점

  • 향상된 보안: 중요한 API 키와 토큰이 백엔드에서만 처리됨
  • 간결한 프론트엔드 코드: 인증 로직이 백엔드로 이동하여 프론트엔드 코드가 단순해짐
  • CORS 문제 해결: 브라우저 리다이렉트를 사용하여 CORS 이슈 방지
  • 통합된 인증 관리: 백엔드에서 모든 소셜 로그인을 일관된 방식으로 처리

단점

  • 백엔드 의존성: 프론트엔드 개발이 백엔드 구현에 의존함
  • 제한된 사용자 경험 제어: 인증 흐름의 세부 단계를 세밀하게 제어하기 어려움
  • 디버깅 복잡성: 문제 발생 시 프론트엔드와 백엔드에 걸쳐 디버깅 필요

4. 두 방식의 아키텍처 비교

4.1 데이터 흐름

4.2 보안 관점의 비교

5. 실제 구현 시 고려사항

5.1 환경 설정

백엔드 위임 방식에서 리다이렉트 URL 관리

const handleSocialLogin = (provider: string) => {
  if (provider === KAKAO_LOGIN_LABEL) {
    try {
      const apiBaseUrl = import.meta.env.VITE_API_URL || '';
      const frontendUrl = import.meta.env.VITE_FRONTEND_URL || window.location.origin;

      // 리다이렉트 URL을 명시적으로 전달 (필요한 경우)
      const kakaoAuthUrl = `${apiBaseUrl}/oauth2/authorization/kakao?redirect_uri=${encodeURIComponent(`${frontendUrl}/oauth/redirect`)}`;

      window.location.href = kakaoAuthUrl;
    } catch (error) {
      // 오류 처리
    }
  }
};

5.2 오류 처리의 강화

// OAuthRedirect.tsx의 개선된 오류 처리
try {
  const response = await axios.get(`${apiBaseUrl}/api/oauth/exchange?code=${code}`, {
    withCredentials: true,
  });

  // 성공 처리
} catch (error) {
  // 상세한 오류 정보 로깅
  if (axios.isAxiosError(error)) {
    const statusCode = error.response?.status;
    const errorData = error.response?.data;

    console.error('🔄 토큰 교환 오류:', {
      status: statusCode,
      message: errorData?.message || error.message,
      data: errorData
    });

    // 오류 유형에 따른 맞춤형 메시지
    if (statusCode === 401) {
      setContent('인증이 만료되었습니다. 다시 로그인해주세요.');
    } else if (statusCode === 404) {
      setContent('인증 정보를 찾을 수 없습니다.');
    } else {
      setContent('인증 처리 중 오류가 발생했습니다.');
    }
  } else {
    console.error('🔄 알 수 없는 오류:', error);
    setContent('알 수 없는 오류가 발생했습니다.');
  }

  setTitle('로그인 실패');
  openAlert();
  navigate('/login');
}

6. 사용자 경험 최적화

6.1 로딩 상태 관리

const [loadingState, setLoadingState] = useState('loading'); // 'loading', 'success', 'error'

// 성공 시
setLoadingState('success');
setTimeout(() => {
  navigate('/home', { replace: true });
}, 800);

// JSX
return (
  <div style={{...}}>
    {loadingState === 'loading' && (
      <>
        <div className="spinner" style={{...}}></div>
        <p>로그인 처리 중...</p>
      </>
    )}

    {loadingState === 'success' && (
      <>
        <div className="success-icon" style={{...}}>✓</div>
        <p>로그인 성공!</p>
      </>
    )}

    {loadingState === 'error' && (
      <>
        <div className="error-icon" style={{...}}>!</div>
        <p>로그인 실패</p>
      </>
    )}
  </div>
);

6.2 애니메이션 및 전환 효과

/* OAuthRedirect.module.scss */
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

@keyframes scaleIn {
  0% { transform: scale(0); }
  70% { transform: scale(1.1); }
  100% { transform: scale(1); }
}

.spinner {
  width: 50px;
  height: 50px;
  border: 5px solid rgba(0, 0, 0, 0.1);
  border-radius: 50%;
  border-top: 5px solid #3396F4;
  animation: spin 1s linear infinite;
}

.success-icon {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 60px;
  height: 60px;
  background-color: #4CAF50;
  border-radius: 50%;
  color: white;
  font-size: 30px;
  animation: scaleIn 0.5s ease-out forwards;
}

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  flex-direction: column;
  gap: 16px;
  background: #f9f9f9;
  animation: fadeIn 0.3s ease-in-out;
}

7. 결론

카카오 소셜 로그인 구현 방식은 프론트엔드 주도에서 백엔드 위임으로 진화하고 있습니다. 이 변화는 다음과 같은 이유로 이루어지고 있습니다:

  1. 보안 강화: 민감한 인증 정보의 노출을 최소화
  2. 코드 간소화: 인증 로직을 백엔드로 이동시켜 프론트엔드 코드 단순화
  3. 유지보수 용이성: 인증 로직이 백엔드에 집중되어 관리가 용이해짐
  4. 일관된 인증 시스템: 다양한 소셜 로그인을 동일한 패턴으로 구현 가능

백엔드 위임 방식은 특히 보안이 중요한 애플리케이션에서 권장되며, 프론트엔드와 백엔드의 명확한 책임 분리를 통해 더 견고한 아키텍처를 제공합니다. 다만, 이 방식은 백엔드와의 긴밀한 협력이 필요하므로 개발 초기부터 두 팀 간의 원활한 소통이 중요합니다.

어떤 방식을 선택하든, 사용자에게 안전하고 매끄러운 로그인 경험을 제공하는 것이 가장 중요합니다. 각 프로젝트의 요구사항과 팀 구성에 맞는 최적의 접근법을 선택하고, 지속적인 개선을 통해 최상의 소셜 로그인 경험을 구현하시기 바랍니다.

8. 참고 자료

 

728x90
반응형