💪 Study 참여/코드리뷰 스터디

[ Study ] 실무와 가까워지는 백엔드 개발 스터디.세션_#2

반응형

이번 포스팅은 두 번째 세션에 대한 내용을 복기하면서 어떤 내용이 담겼는지 정리해보려고 포스팅을 하려고 한다!

이번에는 실제로 api를 구현해보는 시간을 가졌었다.

보통 서버에게 HTTP 요청을 보내면 하는 일은 아래 3단계와 같다.

  1. 받은 데이터에 대한 Validation
  2. 데이터 추가 / 조회 / 변경 / 삭제
  3. 결과 반환

한 줄로, 올바른 데이터가 왔는지 확인한 후 그 데이터에 대한 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에서 제공하는 AuthverifyIdToken메서드를 사용하면 유효한지 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 하지 않는다.

다시 돌아가서 stricttrue이고 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요청에 대한 순서를 간략하게 정리해보면 아래와 같은 순서이다.

  1. req.headers.authorization에 접근하여 token 가져오기
  2. token을 가지고 유효한지 검증한 뒤, decoded 하여 userId 값을 뽑아온다.
  3. 사용자가 수정하려고 하는 값을 검증한다. 즉, body에 실려온 값을 검증.
  4. 수정하려고 하는 컬렉션의 문서가 firebase에 존재하는지 확인한다.
  5. 해당 events의 문서가 존재하면 생성 당시에 추가했던 사용자 id값과 일치하는지 확인한다. 즉, 수정 권한 확인
  6. 기존에 DB에 적재되어있는 데이터에 사용자가 수정하려고 하는 데이터로 덮어 씌운다.
  7. 문서에 update함수를 사용하여 저장한다.
// 실제로 업데이트 하는 부분
const updateData = {
  ...eventInfo, // 기존에 DB에 있던 값
  ...validateReq.data.body, // 새로 덮어 씌우려는 값
};
// 문서에 변경할 값을 넣어줍니다.
await ref.update(updateData);
res.json(updateData);

길었던 시간만큼 이번 세션에서 많은 내용들을 배웠던 것 같았다. 처음에는 이게 무슨 소리야..

하면서 들었는데 복습 강의를 제공해주어서 반복하면서 들으니까 리더님께서 무슨 말을 하시는지 어느 정도 이해가 갔다!

이후에도 계속해서 들을 수 있다고 하니 정말 좋은 것 같다. (프로그래머스 👍)

지난 세션과 동일하게 질문도 바로바로 답변해주셔서 수업 내용이 굉장히 알찼다고 생각한다.

이상으로 포스팅을 마침!

반응형