지난 2주 동안 내가 선택한 에어비앤비에 대해 REST API를 작성했다. Node.js를 다뤄본 경험은 있지만 한 두번이 전부였고, 무엇보다 특정 템플릿에 맞춰 코드를 작성하기란 쉽지않았다. 명세서 리스트업을 완료하지 않고 무작정 API를 작성하는 실수를 저지르기도 하고, 내가 설계한 ERD쪽에서 문제가 발생하기도 했다. 그래도 이런 값진 경험들도 하고, 또 발생한 문제들을 해결해 나가면서 배운점들이 도움이 되기 때문에 값진 2주였던것 같다.
API 명세서
API를 구현하면서 먼저 해야 할 것은 내가 어떤 기능을 하는 API를 어떤 경로로 구현할 것인지를 알려주는 명세서를 리스트업 하는 것이다.
우선 필자는 기존에 28개의 API들을 만들 계획으로 명세서를 짰다. 물론 에어비앤비는 28개보다 훨씬 더 많은 API들이 있겠지만...
필자는 사진과 같이 크게 rooms, user로 나눠서 파일들을 구성했다.
Route: URI를 보고 요청의 경로를 지정해줌
->ex : app.post('/app/login', user.login); //유저 로그인 API
Controller: 형식적 validation 검사 후 service, provider로 넘긴다
->예를들어, 요청시 필요한 정보 유무, 요청받은 메서드 경로 등등...
Service, Provider: 논리적 validation 검사 후 Dao로 넘긴다
->예를들면 방을 생성하려는 유저의 status가 'host'가 맞는지 검사한다
Dao: 실제 내 DB와 매핑된다 쉽게 말해 DB를 고치고, 업데이트하고, 정보를 뽑아온다
모든 코드를 설명하면서 포스팅 할 수 없으니 위시리스트 생성 API에 대해서 포스팅 하겠다
먼저 위시리스트를 생성하는 API를 구현해보자.
파일 구성 설명처럼 Route, Controller, Service, Provider, Dao 각각 사용할 것이다.
//userRoute.js
//Route에서 경로 지정
app.post('/app/wishlist/:userId/:roomId, jwtMiddleware, user.createWishlist)
//userController.js
//Route에서 지정한 경로
//형식적 validation 검사 후 service나 provider로 넘김
exports.createWishlist = async (req, res) => {
const userIdResult = req.tokenInfo.userId;
const wishlistName = req.body.name;
const userId = req.params.userId;
const roomId = req.params.roomId;
if(userIdResult != userId) return res.send(errResponse(baseResponse.USER_ID_NOT_MATCH));
const getWishlistResult = await userProvider.retrieveWishlist(userId, wishlistName);
//위시리스트를 새로 만들면서 추가
if(getWishlistResult.length < 1) {
const createWishlistResult = await userService.createWishlist(wishlistName, userId); //트랜잭션 처리로 롤백 넣자
const getCreateWishlist = await userProvider.retrieveWishlist(userId, wishlistName);
const wishlistId = getCreateWishlist[0].wishlist_id;
const addWish = await userService.addWish(userId, roomId, wishlistId);
return res.send(addWish);
}
else {
const wishlistId = getWishlistResult[0].wishlist_id; //이미 존재하는 위시리스트에 추가 할 때..
const getWishResult = await userProvider.retrieveWish(roomId, wishlistId);
if(getWishResult.length >= 1) return res.send(errResponse(baseResponse.SIGNUP_ALREADYEXIST_ROOM));
const addWish = await userService.addWish(userId, roomId, wishlistId);
return res.send(addWish);
}
}
//userService.js
exports.createWishlist = async (name, userId) => {
const connection = await pool.getConnection(async (conn) => conn);
try{
if(!name) return errResponse(baseResponse.SIGNUP_WISHLISTNAME_EMPTY);
await connection.beginTransaction();
const insertParams = [name, userId];
const createWishlist = await userDao.createWishlist(connection, insertParams);
connection.commit();
const wishlistId = createWishlist[0].insertId;
console.log(`${userId}의 위시리스트 ${wishlistId} : ${name}이/가 등록되었습니다.`);
return response(baseResponse.SUCCESS);
}
catch (err) {
connection.rollback();
logger.error(`App - createWishlist Service error\n: ${err.message}`);
return errResponse(baseResponse.DB_ERROR);
}
finally {
connection.release();
}
}
exports.addWish = async (userId, roomId, wishlistId) => {
const connection = await pool.getConnection(async (conn) => conn);
try{
if(!wishlistId) return errResponse(baseResponse.SIGNUP_WISHLSITID_EMPTY);
await connection.beginTransaction();
const insertParams = [roomId, wishlistId];
const addWishMiddle = await userDao.addWish(connection, insertParams);
connection.commit();
console.log(`${userId}번 유저가 ${roomId}번 방을 위시에 추가했습니다.`);
return response(baseResponse.SUCCESS);
}
catch (err) {
connection.rollback();
logger.error(`App - createWishlist Service error\n: ${err.message}`);
return errResponse(baseResponse.DB_ERROR);
}
finally {
connection.release();
}
}
//userDao.js
async function createWishlist(connection, insertParams) {
const createWishlistQuery = `
INSERT INTO wishlist(wishlist_name, user_id, status)
VALUES (?, ?, 'exist');
`;
const createWishlistRow = await connection.query(createWishlistQuery, insertParams);
return createWishlistRow;
}
async function addWish(connection, insertParams) {
const addWishQuery = `
INSERT INTO wish_middle(room_id, wishlist_id, status)
VALUE (?, ?, 'exist');
`;
const addWishRow = await connection.query(addWishQuery, insertParams);
return addWishRow;
}
에어비앤비의 위시리스트 생성 및 추가는 각 방의 '좋아요'를 누를 때 한번에 이루어진다.
위시리스트를 새로 생성하거나, 이미 있는 위시리스트에 추가하는 형식이다.
필자는 유저 아이디와 위시리스트 이름을 같이 provider로 보내면서 두 값이 모두 같은 row들을 뽑아 오는 코드를 짰다.(userProvider)
만약 이 Row에 값이 들어있다면, 이미 생성된 위시리스트에 방을 추가해주고 없다면 위시리스트 생성 후, 방을 추가한다. (userService에서 검사 후 userDao에서 처리)
위 코드 중 userRoute에서 jwtMiddleware가 보일 것이다.
설명한대로 로그인 시, 토큰을 발급해서 그 토큰이 만료될 때까지 토큰을 이용해 권한을 획득하는 방식이다.
필자는 payload안에 유저의 아이디, 유저의 상태(user, host, superhost, non-existent)들을 담았다.
위시리스트 생성 시 유저의 아이디와 이 유저가 non-existent가 아닌지에 대한 여부를 jwt토큰에서 받아왔다.
//userService.js
exports.createWishlist = async (name, userId) => {
const connection = await pool.getConnection(async (conn) => conn);
try{
if(!name) return errResponse(baseResponse.SIGNUP_WISHLISTNAME_EMPTY);
await connection.beginTransaction();
const insertParams = [name, userId];
const createWishlist = await userDao.createWishlist(connection, insertParams);
connection.commit();
const wishlistId = createWishlist[0].insertId;
console.log(`${userId}의 위시리스트 ${wishlistId} : ${name}이/가 등록되었습니다.`);
return response(baseResponse.SUCCESS);
}
catch (err) {
connection.rollback();
logger.error(`App - createWishlist Service error\n: ${err.message}`);
return errResponse(baseResponse.DB_ERROR);
}
finally {
connection.release();
}
}
위의 코드에서 보면 connetcion.beginTransaction(); connection.commit(), connection.rollback() 과 같은 코드들이 보일 것이다.
각각, 트랜잭션 시작, DB의 변경사항 저장(절대 되돌릴 수 없음), 에러가 나면 커밋하기 전에 되돌리기 의 기능들을 가진다.
위와 같은 처리들을 트랜잭션 처리라고 한다.
위 코드와 같이 데이터베이스의 내용을 변경 저장할 때 이 처리들을 꼭 사용해주도록 하자!
이번 과제 중 필자를 제일 오랫동안 괴롭힌 카카오 소셜 로그인 기능이다. 위에서 JWT를 설명할 때 언급한 3가지의 로그인 방식 중 다른 한가지인 OAUTH방식이다.
ex)카카오 소셜 로그인과정
클라이언트가 카카오 서버에 로그인을 요청 -> 카카오 서버가 인가 코드를 보냄 -> 서버가 인가 코드를 통해 다시 토큰 요청 -> 카카오 서버에서 확인 후 토큰 발급-> 서버에서 로그인 한 사용자 정보 요청 -> 사용자 정보 보냄
이러한 방식으로 진행된다. 필자는 클라이언트의 역할을 수행 할 프론트 페이지를 따로 만들지 않고 Postman에서 진행했다.
먼저 사진과 같이 kakao developers에 접속하여 테스트 할 어플리케이션을 생성한다.
생성 후 [요약정보]에서 REST_API 키와 [카카오 로그인]에서 Redirect_URI 값들을 꼭 기억해 준다.
https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}
포스트맨에서 위와 같은 url에 GET 요청을 보낸다.
요청 후 위 url로 직접 접속을 한다.
그럼 redirect_uri로 설정해준 경로로 접속되어 다음과 같은 페이지를 보여준다. 위 주소창에 ?code=' '를 찾아서 코드값을 복사해준다.
필자는 토큰받기, 유저정보 받아오기 API를 미리 구현해 놓았다. 그래서 필자가 지정한 URI로 요청을 보낸다. 코드값은 body로 보냈다.
이후 axios 모듈을 사용하여 요청을 한번에 처리해줬다.
카카오 로그인을 통한 회원가입 결과이다.