이번 포스팅은 두 번째 세션에 대한 내용을 복기하면서 어떤 내용이 담겼는지 정리해보려고 포스팅을 하려고 한다!
이번에는 실제로 api를 구현해보는 시간을 가졌었다.
보통 서버에게 HTTP 요청을 보내면 하는 일은 아래 3단계와 같다.
- 받은 데이터에 대한 Validation
- 데이터 추가 / 조회 / 변경 / 삭제
- 결과 반환
한 줄로, 올바른 데이터가 왔는지 확인한 후 그 데이터에 대한 CURD 연산을 수행한 뒤 응답을 반환한다.
만약 올바른 데이터가 들어오지 않았다면 바로 결과를 반환해준다.
Next.js Routing
이전에 Next 프로젝트의 라우팅 처리 방식에 대해서 알아보려고 한다.
우선 이전 시간에 살짝 언급했었는데 페이지 개념을 기반으로 한 파일 시스템 기반 라우터가 존재한다.
이게 무슨 말이냐면.. Next는 pages
폴더에 추가된 파일들의 라우트를 자동 생성한다.
그러게 되면 pages/api
폴더 이외의 모든 파일들은 html로 반환된다.
여기서 특이한 점으로는 동적 라우팅의 경우 대괄호를 사용하여 Request 객체를 대괄호 안의 값을 사용할 수 있다.
pages/api/events/[eventsId]/orders/index.ts
--> /api/events/:eventId/orders
와 같이 매핑된다.
하나의 이벤트 가져오기
서버에서는 클라이언트로부터 받은 데이터가 유효한 데이터인지 확인해야 하는 과정이 필요하다고 위에서 설명했다.
넘어오는 값이 단순하다면..
const eventId = req.query.eventId
// 구조 분해 할당 방식
const { eventId } = req.query;
if (eventId === undefined) {
return res.status(400).send('bad request');
}
위와 같이 검증할 수 있다. 하지만 요청으로 많은 값이 들어와 검증해야 할 데이터가 한두 개가 아니라면 불필요한 과정이 많다!
따라서 node.js에서 사용하는 프레임워크인 Fastify에서는 Ajv라는 모듈을 사용해서 유효성 검증을 할 수 있다.
이 모듈은 우리가 검증해야 하는 데이터의 스키마를 정의하고 정의한 스키마대로 데이터가 들어왔는지 확인해주는 모듈이다.
이는 Nest에서 DTO를 사용하여 데이터 validation을 하는 것과 비슷한 느낌인 듯했다!
따라서 우리가 받아야 할 데이터 스키마를 정의해주고 ajv 모듈을 설치해서 data validation을 수행할 수 있다.
아래와 같이 간단하게 바뀔 수 있다.
const validateReq = validate<IFindEventReq>(
{
params: req.query as any,
},
JSCFindEvent,
);
위의 로직을 타면 validateReq.result
에는 데이터 검증에 대한 결과가 들어가게 된다. (true or false)
테스트를 해보면 hello
라는 이벤트를 GET
요청해보자 localhost:4001/api/events/hello
그러면
[masa:api:events:[eventId]:index] GET
[masa:api:events:[eventId]:index] {"eventId":"hello"}
[masa:api:events:[eventId]:index] validateReq.result: true
라는 로그를 볼 수 있다. (firestore에 hello라는 events를 추가하고 test)
이제 특정 이벤트가 있다는 것을 확인했으니 해당 이벤트 내에 존재하는 데이터들을 뽑아보자.
// 참조할 collections을 특정하고 위에서 검증된 eventId에 해당하는 문서를 가리킴
const ref = admin.firestore().collection('events').doc(validateReq.data.params.eventId);
// 가리킨 문서에서 모든 field 값들을 비동기로 가져옴
const doc = await ref.get();
// 문서가 없는 경우
if (doc.exists === false) {
return res.status(404).send('Not found');
}
마지막으로 응답으로 반환하는 과정으로 doc
에서 뽑아온 모든 데이터와 id를 붙여서 반환하도록 한다.
const returnValue = {
...doc.data(),
id: validateReq.data.params.eventId,
};
res.json(returnValue);
그러면 요청 결과는 아래와 같다.
{
"field1": "this is field1",
"id": "hello"
}
그리고 실제 DB에는 아래와 같이 저장이 된 모습이다.
이렇게 하나의 이벤트에 대한 정보를 가져오는 api를 작성해보았다. 실무에서도 위와 같은 방식으로 데이터를 검증하고 가져오거나, 수정 또는 삭제하는 과정을 단순 반복하는 일을 한다.
Next.js의 특징
또 다른 Next.js의 특징으로는 각각의 개별 파일 하나하나가 AWS의 lambda로 치환되어 올라간다고 한다.
이후 Vercel
을 이용해서 배포를 하는 과정이 있는데 배포를 하게 되면
우리가 하는 하나의 서버에서 동작하는 것이 아니라, 각각의 api server가 존재한다고 생각하면 될 것 같다.
따라서 데이터베이스에 접근할 때 init과정도 개별적으로 해주어야 한다.
이때, 초기화가 Duplicate 되진 않았는지 체크하기 위해서 아래와 같은 logic이 필요하다.
if (admin.apps.length === undefined || admin.apps.length === 0) {
// DB에 접근하기 위해서 초기화 진행
admin.initializeApp({
credential: admin.credential.cert({
privateKey: (process.env.privateKey || '').replace(/\\n/g, '\n'),
clientEmail: process.env.clientEmail || '',
projectId: process.env.projectId || '',
}),
});
}
길이가 정의되지 않았거나, 길이가 0이라면 DB를 초기화해주도록 한다.
조건문을 아래와 같이 명시적으로 쓸 수도 있다.
if (!!admin.apps.length === false) { ... }
여기서 replace함수를 간략하게 설명하자면 첫 번째 인자의 값을 두 번째 인자의 값으로 모두 바꿔주세요~이다.
만약 Vercel
을 이용하지 않는다면 초기화를 한 번만 진행하면 된다.
하나의 이벤트 생성하기
한 이벤트를 생성한다는 의미는 firebase의 events
컬렉션에서 새로운 문서를 만든다는 것을 의미한다.
const result = await FirebaseAdmin.getInstance().Firestore.collection('events').add({
... validateReq.data,
});
생성된 결과를 result 변수에 받아준 뒤 반환을 아래와 같이 한다.
const returnValue = {
... validateReq.data,
id: result.id,
};
res.json(returnValue);
추가한 이벤트의 id값까지 추가하여 반환하도록 한다. 이제 아래와 같이 POST 요청을 보내보자.
요청을 보낼 주소 : localhost:4001/api/events
body에 실어 데이터를 보낼 형식은 다음과 같다.
{
"title": "hello",
"owner": {
"uid": "test_uid",
"displayName": "test!!",
"email": "test@gmail.com",
"photoURL": "https://asdfasdf.com/test.jpeg"
}
}
응답으로 생성된 id값이 붙어서 온 것을 확인할 수 있다.
{
"title": "hello",
"desc": "",
"ownerId": "test_uid",
"ownerName": "test!!",
"closed": false,
"id": "sR4t15LeUQVdIQyhCWfx"
}
그러고 실제 DB를 확인하면 아래와 같이 events
컬렉션에 문서가 잘 생성된 모습이다.
실제로 우리가 이전에 만든 한 이벤트를 가져오는 GET 요청을
사용해서 위의 id
값으로 조회를 해보자!
GET 요청을
localhost:4001/api/events/sR4t15LeUQVdIQyhCWfx로
보낸 결과는 아래와 같다.
{
"title": "hello",
"ownerName": "test!!",
"ownerId": "test_uid",
"desc": "",
"closed": false,
"id": "sR4t15LeUQVdIQyhCWfx"
}
인증과 인가 처리하기
앞서 조회, 생성하는 api까지 만들어 보았다. 하지만 이벤트를 생성할 당시 어떤 유저가 만들었는지에 대한 정보는 아직 모른다.
따라서 우리는 이벤트 생성 시 유저의 정보를 저장하도록 하여 그 이벤트에 PUT
요청을 할 때 생성 당시 만들었던 유저인지 확인하기 위한 인증과 인가에 대한 처리를 해보도록 하자.
firebase에서는 자체적으로 인증과 인가에 관련된 기능을 제공해준다.
또한 소셜 로그인까지 간편하게 등록하여 사용자가 어떤 소셜 로그인을 할 지에 대한 선택지도 제공해준다.
따라서 이와 관련된 기능 구현에 그렇게 힘을 쏟을 필요가 없다.
이제 구현하려고 하는 것은 사용자가 어떤 api call을 요청할 때 그 사용자가 유효한지에 대한 check를 하는 것을 추가해보려고 한다.
로그인이 완료되면 브라우저에 인증을 받았다는 토큰을 저장하도록 한다.
이후 HTTP 요청을 보낼 때 함께 토큰을 서버에 전송한다. 토큰에 접근하는 방법은 아래와 같다.
const token = await FirebaseAuthClient.getInstance().Auth.currentUser?.getIdToken();
이렇게 token을 가져와서 firebase에서 제공하는 Auth
의 verifyIdToken
메서드를 사용하면 유효한지 check 할 수 있다.
await FirebaseAdmin.getInstance().Auth.verifyIdToken(token);
이렇게 간단하게 이벤트를 생성할 때 firebase의 Auth를 사용한 인증과 인가에 대해서 알아보았다.
Typescript의 strict option??
HTTP method의 요청에 따라서 예외처리와 분기 처리를 하던 도중,
세션 진행 중 아래와 같은 질문이 있었다.
const supportMethod = ['PUT', 'GET']; // 처리할 Method 정의하기
if (supportMethod.indexOf(method!) === -1) {
return res.status(400).end(); // 다른 요청이 들어온다면 예외처리
}
위의 if 조건문에서 method
뒤에 !
가 붙는 이유에 대해서 질문하였는데 이는 typescript의 strict
라는 옵션과 관련이 있다.
이 옵션은 tsconfig.json
에 존재하며 기본값으로 false
로 설정되어있는데 이를 true
로 바꾸는 것을 추천한다고 한다.
기본적으로 false
라고 설정되어 있으면 undefined를
check 하지 않는다.
다시 돌아가서 strict
가 true
이고 method뒤에 !
가 없다면 아래와 같은 오류를 내뱉는다.
Only valid for request obtained from http.Server.
따라서 이를 해결하려면 아래와 같은 예외 처리하는 구문을 추가로 작성해주어야 한다.
if (method === undifined) {
return res.status(400).end();
}
const supportMethod = ['PUT', 'GET'];
if (supportMethod.indexOf(method) === -1) {
return res.status(400).end();
}
사실 여기서 method
는 항상 존재하기 때문에 강제로 !
를 붙여주어 있다고 강제성을 부여한다는 것으로 이해하면 된다.
하나의 이벤트 수정하기
이제 누가 이벤트를 생성했는지에 대한 정보를 알고 있고, HTTP 요청을 보낼 때 토큰을 가지고 이 사용자가 유효한지 확인도 가능하다.
그러면 이제 이벤트 생성한 사람이 특정 이벤트를 수정하려고 하는 PUT
요청에 대해서 알아보려고 한다.
이때 사용하는 js 문법으로 Spread syntax
를 사용한다.
이전에 PUT
요청에 대한 순서를 간략하게 정리해보면 아래와 같은 순서이다.
- req.headers.authorization에 접근하여 token 가져오기
- token을 가지고 유효한지 검증한 뒤, decoded 하여 userId 값을 뽑아온다.
- 사용자가 수정하려고 하는 값을 검증한다. 즉, body에 실려온 값을 검증.
- 수정하려고 하는 컬렉션의 문서가 firebase에 존재하는지 확인한다.
- 해당 events의 문서가 존재하면 생성 당시에 추가했던 사용자 id값과 일치하는지 확인한다. 즉, 수정 권한 확인
- 기존에 DB에 적재되어있는 데이터에 사용자가 수정하려고 하는 데이터로 덮어 씌운다.
- 문서에
update
함수를 사용하여 저장한다.
// 실제로 업데이트 하는 부분
const updateData = {
...eventInfo, // 기존에 DB에 있던 값
...validateReq.data.body, // 새로 덮어 씌우려는 값
};
// 문서에 변경할 값을 넣어줍니다.
await ref.update(updateData);
res.json(updateData);
길었던 시간만큼 이번 세션에서 많은 내용들을 배웠던 것 같았다. 처음에는 이게 무슨 소리야..
하면서 들었는데 복습 강의를 제공해주어서 반복하면서 들으니까 리더님께서 무슨 말을 하시는지 어느 정도 이해가 갔다!
이후에도 계속해서 들을 수 있다고 하니 정말 좋은 것 같다. (프로그래머스 👍)
지난 세션과 동일하게 질문도 바로바로 답변해주셔서 수업 내용이 굉장히 알찼다고 생각한다.
이상으로 포스팅을 마침!
'💪 Study 참여 > 코드리뷰 스터디' 카테고리의 다른 글
[ Study ] 실무와 가까워지는 백엔드 개발 스터디.세션_#final (4) | 2022.04.22 |
---|---|
[ Study ] 실무와 가까워지는 백엔드 개발 스터디.세션_#3 (0) | 2022.04.12 |
[ Study ] 코드 리뷰를 하는 이유가 무엇일까? (0) | 2022.04.06 |
[ Study ] 실무와 가까워지는 백엔드 개발 스터디.세션_#1 (0) | 2022.03.28 |