[React + Firebase] 카카오 로그인 구현하기 (1)에 이어 카카오 로그인 구현을 해보도록 하겠습니다. 인가 코드를 받아왔다면 REST API로 Access token과 유저 정보를 가져오고, Firebase Authentication을 통해 커스텀 토큰을 생성하여 로그인 처리를 해 주어야 합니다.
먼저 백엔드 코드를 작성하기 위해 Cloud Functions를 사용할 것입니다.
저는 yarn을 사용하고 있기 때문에 아래 명령어로 설치를 진행했습니다.
yarn global add firebase-tools
npm을 사용하고 계신다면 아래 명령어로 설치할 수 있습니다.
npm install -g firebase-tools
firebase login
을 실행하여 브라우저를 통해 로그인하고 Firebase CLI를 인증합니다.firebase init functions
를 실행합니다.cd functions
yarn
이제 functions/src/index.ts
파일에 코드를 작성할 수 있습니다!
구현하고자 하는 API는 다음과 같습니다.
express를 사용하지 않고 구현해도 무방하지만, 추후 확장성을 고려해 express 앱을 HTTP 함수에 전달하는 방법을 사용했습니다. 추가로 cors도 설정해 주도록 합시다.
아래 명령어를 통해 express와 cors를 설치합니다.
yarn add express cors
index.ts
파일에 express 앱을 생성합니다.
import * as functions from "firebase-functions";
import * as express from "express"
import * as cors from "cors"
const app = express();
app.use(cors({ origin: true }));
app.post("/kakao", async (req, res) => {
// TODO: API 구현하기
});
exports.auth = functions.https.onRequest(app);
인가 코드를 이용해 토큰을 가져올 차례입니다.
API 스펙을 보면 client_id
에 REST API 키를 보내야 하므로 환경변수를 이용할 것입니다. 또한 axios를 이용해 HTTP 통신을 처리하도록 하겠습니다. 아래 명령어를 이용해 dotenv와 axios를 설치해 줍니다.
yarn add dotenv axios
index.ts
파일에 코드를 작성합니다. 환경변수는 process.env로 접근할 수 있습니다.
import axios from "axios";
import { config } from "dotenv";
config();
...
interface TokenResponse {
token_type: string;
access_token: string;
id_token?: string;
expires_in: number;
refresh_token: string;
refresh_token_expires_in: number;
scope?: string;
}
const getToken = async (code: string): Promise<TokenResponse> => {
const body = {
grant_type: "authorization_code",
client_id: process.env.KAKAO_REST_API_KEY || "",
redirect_uri: process.env.KAKAO_REDIRECT_URI || "",
code,
};
const res = await axios.post(
"https://kauth.kakao.com/oauth/token",
new URLSearchParams(body)
);
return res.data;
};
app.post("/kakao", async (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({
code: 400,
message: "code is a required parameter.",
});
}
const response = await getToken(code);
const token = response.access_token;
});
...
위 코드에서 주의해야 할 점은 Content-Type이 application/x-www-form-urlencoded
라는 것입니다. getToken
함수를 보면 데이터를 new URLSearchParams()
로 감싸서 보내고 있는 것을 확인하실 수 있습니다.
Access Token으로 사용자 정보를 가져올 수 있게 되었습니다! 👏🏻
API Response를 참고하여 타입을 정의하기 위해 @types
폴더를 생성해 줍니다.
저는 src
폴더 아래에 생성해 주었습니다.
User.ts
파일을 만들고 아래와 같이 정의해 주었습니다.
interface KakaoProfile {
nickname?: string;
thumbnail_image_url?: string;
profile_image_url?: string;
is_default_image?: boolean;
}
interface KakaoAccount {
profile?: KakaoProfile;
name?: string;
email?: string;
birthday?: string;
gender?: "male" | "female";
}
export interface KakaoUser {
id: number;
kakao_account?: KakaoAccount;
}
쉽게 가져오기 위해 @types
폴더에 index.ts
파일을 생성하고 다음과 같이 작성해 주었습니다.
export * from "./User";
src/index.ts
로 돌아와 사용자 정보를 요청하는 코드를 작성합니다.
import { KakaoUser } from "./@types";
...
const getKakaoUser = async (token: string): Promise<KakaoUser> => {
const res = await axios.get(
"https://kapi.kakao.com/v2/user/me",
{ headers: { Authorization: `Bearer ${token}` },
});
return res.data;
};
app.post("/kakao", (req, res) => {
...
const kakaoUser = await getKakaoUser(token);
});
이제 가져온 카카오 유저 정보를 바탕으로 Firebase Authentication을 이용하여 사용자를 생성해주어야 합니다. 이때 사용자와 custom token을 생성하기 위해서는 Admin SDK가 필요하므로 설정을 먼저 진행하도록 합시다.
❶ firebase-admin 설치
yarn add firebase-admin
❷ 서비스 계정의 비공개 키 파일 생성
Firebase 콘솔에서 프로젝트 설정 > 서비스 계정으로 이동하여 새 비공개 키를 생성합니다.
다운로드된 키 파일을 이용해 Admin App을 초기화하기 위해서 Secret Manager를 이용해 환경을 구성합니다. 먼저 구글 콘솔로 이동하여 보안 비밀을 생성합니다.
이름과 다운받은 키 파일을 업로드해 주고 생성합니다. 저는 이미 만들어져 있어 에러가 나고 있는데 무시해 주시면 됩니다.
❹ runWith
생성한 보안 비밀에 접근할 수 있도록 runWith
를 추가합니다.
exports.auth = functions
.runWith({ secrets: ["SERVICE_ACCOUNT_KEY"] })
.https.onRequest(app);
❺ 앱 생성
Admin app이 초기화 되어있는지 확인하고 되어있다면 default app, 되어있지 않다면 초기화하는 코드를 작성합니다.
const getAdminApp = () => {
const serviceAccountKey = JSON.parse(process.env.SERVICE_ACCOUNT_KEY || "");
const app = !admin.apps.length
? admin.initializeApp({
credential: admin.credential.cert(serviceAccountKey),
})
: admin.app();
return app;
};
생성한 app을 이용해 auth service를 불러오고 유저 업데이트를 시도합니다. 만약 유저가 없다면 auth/user-not-found
에러가 나므로, try-catch문을 이용해 유저 생성 코드도 작성해 줍니다. 그 외에 오류 코드 목록은 링크를 참고해 주세요.
import { UserRecord } from "firebase-admin/lib/auth/user-record";
...
const updateOrCreateUser = async (user: KakaoUser): Promise<UserRecord> => {
const app = getAdminApp();
const auth = admin.auth(app);
const kakaoAccount = user.kakao_account;
const properties = {
uid: `kakao:${user.id}`,
provider: "KAKAO",
displayName: kakaoAccount?.profile?.nickname,
email: kakaoAccount?.email,
};
try {
return await auth.updateUser(properties.uid, properties);
} catch (error: any) {
if (error.code === "auth/user-not-found") {
return await auth.createUser(properties);
}
throw error;
}
};
app.post("/kakao", (req, res) => {
...
const authUser = await updateOrCreateUser(kakaoUser);
});
정말 간단하게 생성이 가능합니다.
app.post("/kakao", (req, res) => {
...
const authUser = await updateOrCreateUser(kakaoUser);
const firebaseToken = await admin
.auth()
.createCustomToken(authUser.uid, { provider: "KAKAO" });
return res.status(200).json({ firebaseToken });
});
배포하기 전, 함수가 제대로 작동하는지 확인하고 싶다면 링크를 참고하여 시도하시면 됩니다. 저는 yarn serve
명령어를 이용해 타입스크립트 빌드와 에뮬레이터를 실행하고 Postman으로 테스트를 진행했습니다.
지역을 서울로 설정하고 싶다면 다음과 같이 작성하면 됩니다.
exports.auth = functions
.runWith({ secrets: ["SERVICE_ACCOUNT_KEY"] })
.region("asia-northeast3")
.https.onRequest(app);
아래 명령어를 입력해 함수를 배포합니다.
firebase deploy --only functions
배포가 완료되면 Firebase Console > 모든 제품 > Functions에서 확인하실 수 있습니다.
배포한 함수를 호출하기 위해서는 proxy 설정이 필요합니다. vite로 프로젝트를 생성했다면 간단하게 설정할 수 있습니다.
vite.config.ts
파일로 이동하여 아래와 같이 작성해 줍니다.
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
return {
plugins: [react()],
server: {
proxy: {
"/api": {
target: env.API_URL,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
};
});
.env
파일에 배포한 함수의 URL을 설정하면 끝입니다.
클라이언트에서도 firebase app을 초기화하고 auth를 불러와야합니다. Firebase 시작하기 (Web)를 참고하여 설정하겠습니다.
아래 명령어를 통해 firebase를 설치합니다.
yarn add firebase
src
폴더 아래에 config.ts
파일을 생성하고 Firebase 앱 객체를 만듭니다.
import { initializeApp } from 'firebase/app';
// TODO: Replace the following with your app's Firebase project configuration
const firebaseConfig = {
...
};
const app = initializeApp(firebaseConfig);
위 코드는 Firebase Console 프로젝트 설정 > 일반에서 아래로 스크롤 하면 내 앱 섹션에서 복사해 쉽게 초기화할 수 있습니다.
Firebase 앱을 이용해 auth를 export합니다.
import { getAuth } from "firebase/auth";
...
export const auth = getAuth(app);
이렇게 config.ts
파일 작성을 마무리하고, 다시 콜백 페이지로 넘어와 API 호출과 로그인 코드를 작성하겠습니다.
import axios, { AxiosResponse } from "axios";
import { signInWithCustomToken } from "firebase/auth";
import { auth as firebaseAuth } from "../config";
interface Auth {
firebaseToken: string;
}
const KakaoLogin = () => {
...
const searchParams = new URLSearchParams(location.search);
const code = searchParams.get("code");
if (!code) {
return <Navigate to="/login" />;
}
useEffect(() => {
(async () => {
try {
const res: AxiosResponse<Auth> = await axios.post(
"/api/auth/kakao",
{ code }
);
const { firebaseToken } = res.data;
// custom token을 이용한 로그인
await signInWithCustomToken(firebaseAuth, firebaseToken);
} catch (error) {}
})();
}, []);
...
}
이렇게 하면 로그인이 완료됩니다!
저도 이번 토이 프로젝트를 진행하면서 처음으로 firebase와 typescript를 사용해본 것이라 부족한 부분은 조언 주시면 개선하도록 하겠습니다. 제 글이 조금이라도 도움이 되었으면 좋겠고, 다음 글에서는 회원가입을 통해 유저에 대한 추가 정보를 따로 firestore에 저장하는 방법에 대해 다뤄보겠습니다.
읽어주셔서 감사합니다.
const firebaseToken = await admin.~~ 에서 admin이 존재 하지 않는다고 에러가 발생하는데 추가 설명 가능하시면 부탁드리겠습니다!