NextJS13 + TypeScript로 카카오 로그인 구현하기

ZenTechie·2023년 8월 19일
4

Troubleshooting

목록 보기
3/9
post-thumbnail

기존 React로 진행한 Buddies 프로젝트를 NextJS 13 으로 마이그레이션 하면서, 카카오 로그인 기능을 다시 구현했습니다.

문서를 보면서 구현을 진행하는데 자세하게 설명되어있는 것 같다가도, 뭔가 부실하다고 생각했습니다. 그래서 다른 블로그도 많이 참고했는데, 너무 당연하다고 생각했는지 자세하게 코드를 적지 않은 곳도 많았고 본인만 보려고 작성했는지 이해가 안되는 글도 많았습니다.
그리고 무엇보다도 'NextJS 13 App router + TypeScript'에 관한 글은 단 하나도 찾을 수가 없었습니다. 너무 많은 삽질을 통해 구현을 성공했고, 이에 대해 참고하실 수 있게 포스트를 작성합니다.

들어가기 전..

사용한 기술 스택

본 포스팅은 아래의 버전에 맞춰서 작성되었습니다.

  • NextJS 13 App router 버전 ➡️ pages router 아님 ❌
  • TypeScript

알아둘 것

✅ 카카오 로그인에는 인가 토큰 받기와, 토큰 받기가 있는데 둘이 엄연히 다르다.
✅ 로그인을 완료하려면, 인가 토큰 받기 ➡️ 토큰 받기 순으로 둘 다 수행해야 한다.
NextJS 13 App router의 Route, Route Handler의 개념

미리 설정해야 하는 것

  • 카카오 Developer에서 본인 프로젝트의 카카오 로그인 Redirect Uri 설정
  • 카카오 Developer에서 본인 프로젝트의 웹 사이트 도메인 등록

발생한 에러

  • Kakao is not defined
  • Kakao.init, Kakao.Auth.Authorize is not defined
  • window is not defined
  • POST 500 (Internal Server Error)

에러 해결은 목차를 읽다보면, 나와있습니다.

.env

카카오 로그인 구현에서 사용한 .env 내의 코드이다.

NEXT_PUBLIC_KAKAO_REST_KEY // 카카오 REST API 키
NEXT_PUBLIC_KAKAO_JS_KEY // 카카오 JavaScript 키
NEXT_PUBLIC_KAKAO_LOGIN_REDIRECT_URI=http://localhost:3000/oauth/callback/kakao

디렉토리 구조

이 부분은 내 프로젝트만 해당이 되고 설명이 더 쉽게 이해될 수 있을 것 같아 작성한 것이니,
코드만 가져다가 쓰면 된다.

.
├── app
│   ├── api
│   │   └── oauth
│   │       └── callback
│   │           └── kakao
│   │       		└── route.ts
│   ├── oauth
│   │   └── callback
│   │       └── kakao
│   │       	└── page.tsx
├── components
│   ├── login
│   │   └── Login.ts
  • Login.ts는 login 버튼 컴포넌트가 있는 파일이다.
  • oauth/callback/kakao는 redirect를 처리하는 페이지이다.
  • api/oauth/callback/kakao는 카카오 로그인(접근 토큰)을 처리하는 백엔드이다.

카카오 로그인 로직

카카오 JS SDK 추가 ➡️ 카카오 JS SDK 초기화 ➡️ 로그인 버튼 클릭하여, 카카오 로그인 인가코드 받기 ➡️ 카카오 로그인 토큰 받기


본격적인 로그인 구현하기

1. 카카오 JS SDK 추가

공식 문서의 SDK 다운로드에 가면, 아래의 3개가 존재하는데 나는 Full SDK를 사용했다.

  • Full SDK
  • Full SDK (Uncompressed)
  • Kakao Story Only SDK

초기 Full SDK 코드는 아래와 같은데, 약간의 수정이 필요하다.
crossorigin ➡️ crossOrigin으로 바꿔준다.
(integrity=[민감정보]는 내가 임의로 설정한 것이고, 본인 코드에서는 sha-어쩌구로 잘 보일 것이다.)

<script src="https://t1.kakaocdn.net/kakao_js_sdk/2.3.0/kakao.min.js" integrity=[민감정보] crossorigin="anonymous"></script>

SDK 초기화app/layout.tsx에서 수행한다.

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <head>
      </head>
      <body className={inter.className}>
  		// 호출 위치.
        <Script
          async
          src='https://t1.kakaocdn.net/kakao_js_sdk/2.3.0/kakao.min.js'
          integrity='sha384-70k0rrouSYPWJt7q9rSTKpiTfX6USlMYjZUtr1Du+9o4cGvhPAWxngdtVZDdErlh'
          crossOrigin='anonymous'
        ></Script>
		{children}
      </body>
    </html>
  );
}

나는 next/scriptScript를 사용하여 <body>내 에서 초기화했다. 여기서 async는 작성하지 않아도 된다.

그리고 어떤 글을 보니<head>안에 <Script />를 초기화하던데, 사실 차이는 잘 모르겠다.


2. 카카오 JS SDK 초기화

✅ 여기서부터 매우 중요하다.
✅ SDK 초기화는 JavaScript 키를 사용한다.

먼저 Kakao에 관련된 함수를 사용하려면, SDK 초기화를 진행해야 한다.

코드는 아래와 같다.

// app/components/login/Login.tsx

if (!window.Kakao.isInitialized()) {
  window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
}

하지만 여기서 아래의 에러'들'이 발생할 것이다.

  • window is not defined
  • window.Kakao is not defined
  • window.Kakao.init is not defined 또는 window.Kakao.Auth is not defined

나는 처음에는 이 에러가 발생하지 않아서 카카오 로그인이 바로 됐었다.
근데 참 이상한게, 다음날 하면 또 에러가 발생해서 안되더라 🤬

원인은 정확하지 않지만, 찾아본 바로는 NextJS에서 페이지가 로드되기 전에 window, window.Kakao가 아직 초기화도 되지 않았는데 window.Kakao에 접근하려고 해서 그런다고 한다.

아래의 코드로 해결할 수 있다.

  useEffect(() => {
    console.log('window.Kakao: ', window.Kakao);
    if (window.Kakao) {
      if (!window.Kakao.isInitialized()) {
        window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
        console.log('after Init: ', window.Kakao.isInitialized());
      }
    }
  }, []);

  useEffect(() => {
    if (window.Kakao) {
      if (!window.Kakao.isInitialized()) {
        window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
        console.log('after Init: ', window.Kakao.isInitialized());
      }
    }
  }, [window.Kakao]);

총 2개의 useEffect 훅을 사용했다. 먼저 첫 번째는 페이지가 렌더링 됐을 때 실행을 일단 시켜보는 코드이다. 이때는 window.Kakao가 존재하지 않기 때문에 의도한대로 SDK 초기화가 진행되지는 않을 것이다.

두 번째 useEffect에서 이제 우리가 원하는대로 SDK 초기화가 진행된다.

아직 로직이 끝나지 않았습니다. 전체 코드는 3에 나옵니다.


3. 로그인 버튼 클릭하여, 카카오 로그인 인가코드 받기

먼저 인가코드를 받기 위해서는, 2가지가 필요하다.

  1. redirectUri
  2. scope

redirectUri는 말그대로, 로그인 버튼을 눌렀을 때 이동할 리다이렉트 페이지이다.(위에서 언급했듯이 미리 설정해야 한다.)
scope사용자에게 받으려는 정보를 의미한다. 이것은 본인이 원하는대로 작성하면 된다.

나는 redirectUri와 scope를 아래와 같이 설정했다.

const redirectUri = `http://localhost:3000/oauth/callback/kakao`;
const scope = [
  'profile_nickname',
  'profile_image',
  'account_email',
  'gender',
  'age_range',
  'birthday',
  'friends',
  'openid',
].join(','); // 프로필 이름, 이미지, 계정 이메일, 성별, 나이, 생일, 친구목록, openid를 받아왔다.

설정을 모두했다면, 인가코드를 받는 페이지로 넘어가야하므로 아래의 코드를 호출한다.


// 인가코드 받는 페이지로 넘어가는 코드
window.Kakao.Auth.authorize({
  redirectUri,
  scope,
});

여기까지 작성하고, 잘 실행됐다면 웹의 url이 아래와 같이 변경됐을 것이다.
[본인이 설정한 redirect_url]?code=[인가 코드]

내가 설정한 redirectUrihttp://localhost:3000/oauth/callback/kakao였으므로, 아래와 같이 url이 변경되었다.

이때 본인이 프로젝트를 배포하지 않았다면, redirectUri는 `http://localhost:3000 으로 시작해야 한다.(포트 번호는 다를 수 있다.)

또한, 나는 http://localhost:3000을 계속 작성하는게 귀찮아서, 이를 포함시켜서 redirectUri를 설정했다.

전체코드는 아래와 같다.

// app/components/login/Login.tsx

const redirectUri = `http://localhost:3000/oauth/callback/kakao`;
const scope = [
  'profile_nickname',
  'profile_image',
  'account_email',
  'gender',
  'age_range',
  'birthday',
  'friends',
  'openid',
].join(',');

export default function Login({}: Props) {
  useEffect(() => {
    console.log('window.Kakao: ', window.Kakao);
    if (window.Kakao) {
      if (!window.Kakao.isInitialized()) {
        window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
        console.log('after Init: ', window.Kakao.isInitialized());
      }
    }
  }, []);

  // 카카오 SDK 초기화
  useEffect(() => {
    if (window.Kakao) {
      if (!window.Kakao.isInitialized()) {
        window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
        console.log('after Init: ', window.Kakao.isInitialized());
      }
    }
  }, [window.Kakao]);
  
  const kakaoLoginHandler = () => {
    // 인가 코드 받기 위해서, 리다이렉트 페이지로 이동
    window.Kakao.Auth.authorize({
      redirectUri,
      scope,
    });
    console.log('Kakao Logining'); // 확인용 로그
  };

  return (
	<button onClick={kakaoLoginHandler}>카카오 로그인</button>
  );
}

4. 카카오 로그인 [토큰 받기]

들어가기에 앞서, 다른 블로그 글을 참고하다가 알았는데 토큰을 프론트엔드에서 처리하게 되면 카카오에서 고소를 할 수 있다고 한다.(사실인지는 모름)

이전에 프론트엔드에서 모두 구현한 코드가 있었는데, 위와 같은 이유때문에 백엔드에서 처리하도록 코드를 수정했다.

대망의 마지막 챕터이다. 🎉

우리가 살펴볼 로직의 순서는 다음과 같다.
1. 3에서의 url에 포함된 인가 코드 가져오기
2. 인가 코드를 토대로 POST요청에 필요한 url을 만들고, 백엔드에 POST 요청하기
3. 백엔드에서 인가 코드를 추출하고, 인가 코드로 실제 토큰 받기 수행

토큰 받기를 수행하려면, 인가토큰이 꼭 필요하다.
✅ 이제부터의 코드는 백엔드 그리고 redirectUri를 렌더링하는 페이지에 관한 코드이다.


4-1. url에서 인가코드 가져오기

먼저, 3에서의 http://localhost:3000/oauth/callback/kakao?code=[인가 코드](redirect된 url)에서의 인가코드가 필요하다.

인가코드는 다음과 같이 가져올 수 있다.

const searchParams = useSearchParams();
const authCode = searchParams.get('code'); // 인가코드가 저장된다.

4-2. 인가 코드를 토대로, 백엔드에 POST 요청하기

NextJS 13 App router의 Route, Route Handlers에 관한 개념을 이미 숙지하고 있다는 가정하에 진행합니다.

일단, 백엔드 디렉토리를 만들어보자. 실제로 토큰 받기를 수행하는 곳은 여기에서 모두 담당한다.
나는 app/api/oauth/callback/kakao로 만들었다.

잠깐..

백엔드 디렉토리를 보면, api를 제외하고 oauth/callback/kakaoNEXT_PUBLIC_KAKAO_LOGIN_REDIRECT_URI동일한 것을 알 수 있다.

굳이 동일하지 않아도 된다.
나는 그냥 카카오에 관련된 기능을 수행하는 곳이기에 구별하기 편하려고 동일한 이름으로 디렉토리를 설정했다.

자, 이제 백엔드에 POST 요청을 해보자. 나는 이를 수행할 함수(loginHandler)를 하나 만들었다.

// 매개변수 code는 인가 코드를 의미한다.
const loginHandler = async (code: string | string[]) => {
    const res = await axios.post(`/api/oauth/callback/kakao?code=${code}`);

    const data = res.data;
    console.log("data returned from api: ", data);
};

여기서 중요한 것은 post 요청을 보내는 곳이다.

요청은 어디로 보내야 하는걸까?
➡️ 위에서 설정한 백엔드의 디렉토리 경로로 설정해야 하고, code(인가 코드)queryString으로 포함시켜서 보낸다.

위 코드대로만 하면 잘 수행되나?
➡️ 그건 또 아니다. 페이지가 렌더링 되기 전 인가코드를 가져오려고 하기 때문에 생각한대로 인가코드가 저장되지 않을 수 있다.

그럼 어떻게 해결하나?
➡️ 인가 코드가 저장되는 변수의존성으로 설정하여, useEffect 훅을 호출한다.


해결을 포함한 전체 코드도 첨부하겠다.

export default function KakaoTalk() {
  const searchParams = useSearchParams();
  const authCode = searchParams.get('code'); // 인가 코드가 저장되는 변수
  
  const loginHandler = async (code: string | string[]) => {
    const res = await axios.post(`/api/oauth/callback/kakao?code=${code}`);

    const data = res.data;
    console.log("data returned from api: ", data); // 데이터 잘 받아오는지 확인용 로그
  };

  // 인가 코드가 저장될 수 있도록 하는 useEffect 훅
  useEffect(() => {
    if (authCode) { // 인가 코드가 있을 때만 POST 요청을 보낸다.
      loginHandler(authCode);
    }
  }, [authCode]); // 의존성으로 인가 코드가 저장되는 변수를 사용한다.

  return <div>page</div>;
}

4-3. 백엔드에서 인가 코드를 추출하고, 인가 코드로 실제 토큰 받기 수행하기

일단, 전체 코드를 먼저 보고 설명을 보도록 하자.

import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

// 함수 이름은 아래와 동일해야 한다.
export async function POST(req: NextRequest) {
  const url = new URL(req.url); // url 객체 생성
  const code = url.searchParams.get('code'); // code 데이터 추출
  try {
    const res = await axios(
      `https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=${process.env.NEXT_PUBLIC_KAKAO_REST_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_LOGIN_REDIRECT_URI}&code=${code}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      }
    );

    const data = res.data;
    return NextResponse.json({
      data: data,
    });
  } catch (e) {
    return NextResponse.json({
      data: 'fail',
    });
  }
}

앞서 queryString으로 code를 포함시켜서 post 요청을 보냈었다. 이를 추출하려면 다음 코드를 호출해야 한다.

const url = new URL(req.url); // url 객체를 생성한다.
const code = url.searchParams.get('code'); // code 데이터를 추출한다.

이제 정말 다 끝났다. 인가코드까지 추출했으니 이를 포함시켜서 토큰 받기 요청을 진행하면 된다.

요청에 관련된 자세한 구조는 공식 문서에서 참고하길 바란다.

const res = await axios(
      `https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=${process.env.NEXT_PUBLIC_KAKAO_REST_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_LOGIN_REDIRECT_URI}&code=${code}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      }
);

✅ 여기서 post 요청의 url을 잘 살펴봐야 한다.
적어도 본 포스팅의 코드에서는 redirect_uri=${window.href.origin} 으로 작성하면 절대 안된다.
이렇게 작성하면 POST 500 (Internal Server Error)가 발생할 것이다. 매우 높은 확률로... 이는 redirectUri가 우리가 미리 설정한 카카오 프로젝트의 redirectUri와 일치하지 않아서 발생하는 문제이다.

실제로 나도 이거 때문에 오류가 계속 발생했었다.

✅ post 요청의 url에 모든 것을 포함하지 않아도 된다.(아마도..)
➡️ 기본 url을 https://kauth.kakao.com/oauth/token로 설정하고, 나머지는 data에 넣어서 요청해도 된다.

axios POST 요청을 잘 모르겠으면, 공식 문서를 보자.

자 이제 작성할 코드는 모두 끝이다. 이제 직접 수행을 해보고 결과를 봐보자!

로그인 결과는?

성공적이다.
인가코드도 잘 받아왔고 이를 토대로 프론트엔드에서 토큰을 직접 처리하지 않고 백엔드에서 잘 처리했고, 로그인도 성공적으로 수행됐다.

느낀 점..

솔직히, 처음에 카카오 로그인은 금방 끝낼 수 있을 거라고 생각했는데 생각보다 복병이었다.
문서를 보면 자세하게 설명되어있는 것 같다가도, 뭔가 부실하다고 생각했다.
그래서 다른 블로그도 많이 참고했는데, 너무 당연하다고 생각했는지 자세하게 코드를 적지 않은 곳도 많았고 본인만 보려고 작성했는지 이해가 안되는 글도 많았다.

그리고 무엇보다도 'NextJS 13 App router + TypeScript'에 관한 글은 단 하나도 찾을 수가 없어서.. 구현하기가 매우 까다로웠고 시간도 너무 많이 쏟았다.(아니면 내가 그냥 멍청한 걸 수도 있다)

🎉 너무 안풀려서 자다가도 로직 생각하면서 머릿속으로 그림을 그려보기도 하고, 별 짓을 다했는데 결국엔 구현하니 속이 시원하다.

나와 같이 어려움을 겪지 않고 다른 분들은 그냥 쉽게 쉽게 구현했으면, 그리고 나중에 내가 참고하기 위해서 포스팅을 최대한 자세하게 이해하기 쉽게 작성해보려고 했는데, 잘 됐는지는 모르겠다.

나는 토큰을 이용해서 다른 기능도 추가해야 했기 때문에 사용하진 않았지만, 그냥 단순 구현이 필요하면 next-auth 라이브러리 쓰시길..
한번 사용해봤는데 너무나 허무하게도 5분도 안되서 구현.. 🤮

제발 여러분은 시간 쏟지 말고 한번에 구현하시길..


다시 발생한 오류

Kakao SDK 초기화가 안되는 오류가 또 다시 발생했다.
다른 블로그 글을 참고해서 아래의 코드를 추가해서 해결했다.

// app/components/login/LoginSection.tsx

useEffect(() => {
    const kakaoSDK = document.createElement('script');
    kakaoSDK.async = false;
    kakaoSDK.src = `https://t1.kakaocdn.net/kakao_js_sdk/2.3.0/kakao.min.js`;
    kakaoSDK.integrity = `본인 integrity`;
    kakaoSDK.crossOrigin = `anonymous`;
    document.head.appendChild(kakaoSDK);

    const onLoadKakaoAPI = () => {
      if (!window.Kakao.isInitialized()) {
        window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
        console.log('after Init: ', window.Kakao.isInitialized());
      }
    };

    kakaoSDK.addEventListener('load', onLoadKakaoAPI);
  }, []);

(나의 경우 위의 코드를 추가한 곳은 로그인 페이지를 의미하는 LoginSection이다.)


참고

NextJS 백엔드에서 URL 접근하기

profile
데브코스 진행 중.. ~ 2024.03

4개의 댓글

comment-user-thumbnail
2023년 11월 7일

선생님,, 덕분에 시간 많이 아낀 것 같습니다 감사합니다 ㅠ^ㅠ

답글 달기
comment-user-thumbnail
2024년 3월 25일

감사합니다 모니터 목숨 구하셨어요

답글 달기
comment-user-thumbnail
2024년 4월 11일

환경변수에 NEXT_PUBLIC_ 을 붙여도 보안상 문제가 없을까요? (붙이신 이유가 있을까요?)

1개의 답글