카카오 로그인 with Next 13, react-query, strapi
2023. 5. 29. 01:38ㆍWeb_Programming/Next
Next 13에서 카카오 로그인을 수행하는 과정을 포스팅해보려합니다.
개발환경은 다음과 같습니다.
Next 13, react-query, Strapi (CMS)
Next api route 에서 카카오 로그인을 처리하고
User에 대한 접근은 strapi의 API를 통해 수행하였습니다.
로그인 로직은 크게
1. 기본 로그인 (useKakaoAuth)
2. 자동 로그인 및 로그인 검사 (useAuth)
로 나눠집니다.
두 로직 각각의 흐름과 코드에 대해 설명해보겠습니다.
1. useAuthKakao
기본 로그인 로직 흐름은 다음과 같습니다.
querystring code 값 가져오기
const searchParams = useSearchParams();
const query = searchParams?.get("code");
useEffect(() => {
if (query) {
getKakaoToken.mutate(query);
}
}, [query]);
※ next 13 에서는 useRouter가 아닌, useSearchParams 로 query를 가져올 수 있습니다.
따라서,
useEffect hooks에서 query의 변경 시, code를 넘겨 카카오 AccessToken을 가져옵니다.
GET Kakao access_token
const getKakaoToken = useMutation({
mutationFn: async (code: string) => {
const data = {
grant_type: "authorization_code",
client_id: process.env.NEXT_PUBLIC_KAKAO_REST_KEY || "",
redirect_uri: "/*카카오 dev에서 설정한 redirect uri*/",
code,
};
const queryString = Object.keys(data)
.map((key) => `${key}=${data[key as keyof typeof data]}`)
.join("&");
return await axios.post(
"https://kauth.kakao.com/oauth/token",
queryString,
{
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8",
},
}
);
},
onSuccess: (res: any) => {
putKakaoTokenToServer.mutate(res.data.access_token);
},
retry: false,
});
react-query를 사용한 코드입니다.
kakao API KEY, redirect uri, code를 담은 데이터로
카카오 AccessToken을 받아오는 API를 요청
성공시, 해당 토큰으로 서버에 로그인 요청을 보냅니다.
server에 로그인 요청
const putKakaoTokenToServer = useMutation({
mutationFn: async (token: string) => {
return await axios.put("/api/auth/kakao", {
access_token: token,
});
},
});
토큰을 담아 서버에 요청을 보냅니다.
next 내의 /api/auth/kakao route로 요청
+ next 13에서 api 폴더 위치는 아래와 같습니다.
src/app/api/하위 route
GET 카카오 user (user 식별 정보)
const result = await KakaoAuth.getProfile(access_token);
const kakaoUser = JSON.parse(result).kakao_account;
let userInfo = {
email: kakaoUser.email,
userName: kakaoUser.profile.nickname,
id: -1,
};
↓ 하위 코드와 같이 모듈로 분리한 카카오 프로필 정보 요청 함수를 통해 user 정보를 가져옵니다.
DB에서 식별정보로 user의 email을 사용하고 있기 때문에
이를 이용하여 DB의 user에 접근합니다.
const request = require("request");
export default {
getProfile(accessToken: string) {
return new Promise((resolve, reject) => {
request(
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
url: "https://kapi.kakao.com/v2/user/me",
method: "GET",
},
(error: any, response: any, body: any) => {
if (!error && response.statusCode === 200) {
resolve(body);
}
reject(error);
}
);
});
},
};
DB user Find or Create
const { attributes } = await strapiAuthUsersApi.getUser(userInfo.email);
userInfo.id = attributes.id;
if (!attributes) {
const { attributes: created_attributes } =
await strapiAuthUsersApi.createUser({
userInfo,
kakaoToken: access_token,
});
userInfo.id = created_attributes.id;
}
↓ 아래와 같은 strapi API 모듈로 email을 통해 사용자를 가져오거나 생성 시켜줍니다.
import axios from "axios";
const getUser = async (email: string) => {
try {
const {
data: { data },
} = await axios.get(
`http://localhost:1337/api/auth-users?filters[email][$eq]=${email}`
);
return { attributes: data[0].attributes };
} catch (error) {
console.error(error);
return { attributes: undefined };
}
};
const createUser = async ({
userInfo,
kakaoToken,
}: {
userInfo: {
email: string;
userName: string;
id: number;
};
kakaoToken: string;
}) => {
try {
const {
data: { data },
} = await axios.post("http://localhost:1337/api/auth-users", {
data: { ...userInfo, kakaoToken },
});
return { attributes: data.attributes };
} catch (error) {
console.error(error);
return { attributes: undefined };
}
};
export const strapiAuthUsersApi = { getUser, createUser };
user 정보 + jwt 토큰
return new Response(
JSON.stringify({
success: true,
jwt: jwtUtil.getJWTToken(userInfo),
user: { ...userInfo },
}),
{
status: !attributes ? 201 : 200,
}
);
위에서 가져온 user 정보와 함께
JWT 토큰을 발행하여 응답해줍니다.
const jwt = require("jsonwebtoken");
const getJWTToken = (userInfo: { email: string; userName: string }) => {
return jwt.sign(
{
...userInfo,
},
process.env.NEXT_SERVER_JWT_SECRET,
{
issuer: "bbangsoon",
}
);
};
const getJWTUser = (jwtToken: string) => {
return jwt.verify(jwtToken, process.env.NEXT_SERVER_JWT_SECRET);
};
export const jwtUtil = { getJWTToken, getJWTUser };
전체 서버 코드
import axios from "axios";
import KakaoAuth from "../utils/KakaoAuth";
import { jwtUtil } from "../utils/util";
import { strapiAuthUsersApi } from "@lib/apis/AuthUsersApis";
export async function PUT(req: Request) {
try {
const { access_token } = await req.json();
if (!access_token)
return new Response("token 없음.", {
status: 401,
});
const result: any = await KakaoAuth.getProfile(access_token);
const kakaoUser = JSON.parse(result).kakao_account;
let userInfo = {
email: kakaoUser.email,
userName: kakaoUser.profile.nickname,
id: -1,
};
const { attributes } = await strapiAuthUsersApi.getUser(userInfo.email);
userInfo.id = attributes.id;
if (!attributes) {
const {
data: { data },
} = await axios.post("http://localhost:1337/api/auth-users", {
data: { ...userInfo, kakaoToken: access_token },
});
userInfo.id = data.attributes.id;
}
return new Response(
JSON.stringify({
success: true,
jwt: jwtUtil.getJWTToken(userInfo),
user: { ...userInfo },
}),
{
status: !attributes ? 201 : 200,
}
);
} catch (err: any) {
return new Response("test", {
status: 500,
});
}
}
로그인 완료
서버로 전달 받은 jwt 토큰을
자동 로그인을 위해, localStorage에 저장 해주고
이후 API 요청에 인증을 확인하기 위해
jwt토큰으로 Authrization 헤더 항목을 설정해줍니다.
const putKakaoTokenToServer = useMutation({
mutationFn: async (token: string) => {
return await axios.put("/api/auth/kakao", {
access_token: token,
});
},
onSuccess: (res: any) => {
const { jwt, user } = res.data;
if (res.status == 201 || res.status == 200) {
setUserAtom(user);
window.localStorage.setItem(
"token",
JSON.stringify({
access_token: jwt,
})
);
axios.defaults.headers.common["Authorization"] = `${jwt}`;
router.push("/home");
} else {
window.alert("로그인에 실패하였습니다.");
resetUserAtom();
}
},
});
전체 useAuthKakao 코드
import { userInfoAtoms } from "@app/GlobalProvider";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { useSetAtom } from "jotai";
import { useResetAtom } from "jotai/utils";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
const useAuthKakao = () => {
const router = useRouter();
const searchParams = useSearchParams();
const query = searchParams?.get("code");
const setUserAtom = useSetAtom(userInfoAtoms.userAtom);
const resetUserAtom = useResetAtom(userInfoAtoms.userAtom);
const getKakaoToken = useMutation({
mutationFn: async (code: string) => {
const data = {
grant_type: "authorization_code",
client_id: process.env.NEXT_PUBLIC_KAKAO_REST_KEY || "",
redirect_uri: "http://localhost:3000/auth/login",
code,
};
const queryString = Object.keys(data)
.map((key) => `${key}=${data[key as keyof typeof data]}`)
.join("&");
return await axios.post(
"https://kauth.kakao.com/oauth/token",
queryString,
{
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8",
},
}
);
},
onSuccess: (res: any) => {
putKakaoTokenToServer.mutate(res.data.access_token);
},
retry: false,
});
const putKakaoTokenToServer = useMutation({
mutationFn: async (token: string) => {
return await axios.put("/api/auth/kakao", {
access_token: token,
});
},
onSuccess: (res: any) => {
const { jwt, user } = res.data;
if (res.status == 201 || res.status == 200) {
setUserAtom(user);
window.localStorage.setItem(
"token",
JSON.stringify({
access_token: jwt,
})
);
axios.defaults.headers.common["Authorization"] = `${jwt}`;
router.push("/home");
} else {
window.alert("로그인에 실패하였습니다.");
resetUserAtom();
}
},
});
useEffect(() => {
if (query) {
getKakaoToken.mutate(query);
}
}, [query]);
return {
isLoading: getKakaoToken.isLoading || putKakaoTokenToServer.isLoading,
};
};
export default useAuthKakao;
2. useAuth
카카오 로그인 이후 로그인 검사는 다음과 같이 이루어집니다.
localStorage에서 jwt 토큰 가져오기
const [jwtToken, setJwtToken] = useState(null);
useEffect(() => {
const token = JSON.parse(
window?.localStorage.getItem("token") || "null"
)?.access_token;
if (token) {
setJwtToken(token);
axios.defaults.headers.common["Authorization"] = `${token}`;
}
}, [])
localStorage의 token을 검사하여 있다면,
헤더의 인증정보를 같이 설정해줍니다.
server에 auth check
useQuery(["getAuth"], {
queryFn: async () => {
return await axios.get("/api/auth", {
headers: { Authorization: `${jwtToken}` },
});
},
onSuccess: (res: any) => {
setUserAtom(res.data.user);
},
onError: (err: any) => {
console.error(err);
resetUserAtom();
window.localStorage.removeItem("token");
},
enabled: !!jwtToken,
retry: false,
});
jwtToken이 존재하면,
서버에 인증 요청을 보냅니다.
※ 에러 발생시, user 정보를 Reset하고 (로그아웃) 토큰을 지웁니다.
jwt 토큰 으로 user 식별 정보 가져오기
const jwtUser = jwtUtil.getJWTUser(
req.headers.get("authorization") as string
);
let userInfo = {
email: jwtUser.email,
userName: jwtUser.userName,
id: -1,
};
const jwt = require("jsonwebtoken");
const getJWTUser = (jwtToken: string) => {
return jwt.verify(jwtToken, process.env.NEXT_SERVER_JWT_SECRET);
};
export const jwtUtil = { getJWTToken, getJWTUser };
DB user 가져오기
위에서 가져온 JWT user의 email로 DB user에 접근합니다.
const { attributes } = await strapiAuthUsersApi.getUser(userInfo.email);
userInfo.id = attributes.id;
if (!attributes)
return new Response("user 없음.", {
status: 404,
});
여기서 정보가 없을 경우 404 에러를 보냅니다.
전체 서버 코드
import { strapiAuthUsersApi } from "@lib/apis/AuthUsersApis";
import { jwtUtil } from "./utils/util";
export async function GET(req: Request) {
try {
if (!req.headers.get("authorization"))
return new Response("token 없음.", {
status: 401,
});
const jwtUser = jwtUtil.getJWTUser(
req.headers.get("authorization") as string
);
let userInfo = {
email: jwtUser.email,
userName: jwtUser.userName,
id: -1,
};
const { attributes } = await strapiAuthUsersApi.getUser(userInfo.email);
userInfo.id = attributes.id;
if (!attributes)
return new Response("user 없음.", {
status: 404,
});
return new Response(
JSON.stringify({
success: true,
jwt: jwtUtil.getJWTToken(userInfo),
user: { ...userInfo },
}),
{
status: 200,
}
);
} catch (err: any) {
return new Response("test", {
status: 500,
});
}
}
전체 useAuth 코드
import { userInfoAtoms } from "@app/GlobalProvider";
import { useMutation, useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useAtomValue, useSetAtom } from "jotai";
import { useResetAtom } from "jotai/utils";
import { useEffect, useState } from "react";
const useAuth = () => {
const [jwtToken, setJwtToken] = useState(null);
const setUserAtom = useSetAtom(userInfoAtoms.userAtom);
const user = useAtomValue(userInfoAtoms.userAtom);
const resetUserAtom = useResetAtom(userInfoAtoms.userAtom);
useQuery(["getAuth"], {
queryFn: async () => {
return await axios.get("/api/auth", {
headers: { Authorization: `${jwtToken}` },
});
},
onSuccess: (res: any) => {
setUserAtom(res.data.user);
},
onError: (err: any) => {
console.log(err);
resetUserAtom();
window.localStorage.removeItem("token");
},
enabled: !!jwtToken,
retry: false,
});
useEffect(() => {
const token = JSON.parse(
window?.localStorage.getItem("token") || "null"
)?.access_token;
if (token) {
setJwtToken(token);
axios.defaults.headers.common["Authorization"] = `${jwtToken}`;
}
}, []);
return;
};
export default useAuth;
이렇게 해서 Next 13, react-query, Strapi를 이용하여 카카오 로그인을 구현해봤습니다.
따로 서버를 두지 않고 Next의 장점인 프론트 서버를 구축해볼 수 있었고,
strapi로 직관적이고 간단하게 DB와 API를 이용하면서
프론트에 더 집중하여 코드를 작성해 볼 수 있었습니다.
궁금하시거나, 문제되는 부분이 있다면 댓글로 알려주세요!
'Web_Programming > Next' 카테고리의 다른 글
Next router 이동 막기 _알림창 (1) | 2022.12.04 |
---|---|
Next의 Pre-rendering과 함수들 (0) | 2022.11.01 |
Next의 Redirect | next.config, Middleware (0) | 2022.10.28 |
ClientSide에서 rendering 전 redirect 가능한가? (0) | 2022.10.06 |