문제 해결 - 2차 플젝

Sinf·2022년 5월 18일
1

고민의 흔적

목록 보기
22/38
post-thumbnail

문제 해결

프로젝트를 진행하면서 생긴 문제와 해결에 대해서 글로 정리한 것이 없어 노션에 저장된 기록들을 하나의 글로 정리하려고 한다.

Typescript ESLint 적용

Unable to resolve path to module './app'

에러 발생, 해당 문제는 eslint 설정의 문제.

.eslintrc.json에 다음과 같은 설정을 추가했다.

"settings": {
  "import/resolver": {
    "node": {
      "extensions": [".js", ".ts"]
    },
    "typescript": {}
  }
}

Missing file extension "ts" for

에러 발생, 이 또한 설정 문제.

.eslintrc.jsonrules"import/extensions": 0를 추가했다.

Typescript에서 MongoDB

@types/node

Property 'end' in type 'GridFSBucketWriteStream' is
not assignable to the same property in base type
'WritableStream'.

구글 검색 시 사람들마다 해결 방법이 분분했다.

  1. tsconfig.json에서 strictNullChecks가 문제다.
  2. @types/node 설치해라.
  3. package-lock.json을 지우고 다시 설치해라.

일단, @types/node를 설치했다. 바로 해결되지 않았는데, Node는 16버전인데, @types/node가 17 버전대로 설치되었다. 그래서 @types/node를 16 버전으로 내렸더니 해결되었다.

Document Type

Typescript를 적용하면서, 타입에 대해 신경 써야하는 것들이 생겼다.
Post에 대한 정보를 조회하는 경우, 바로 비구조화를 통해 데이터를 사용했는데, 필요없는 값들이 생겨났다.

const post = await Post.findOne();
const { title, ...rest } = post;

위와 같은 코드에서 rest에 Schema와 관련된 정보가 담겨있었다.

가져오는 타입이 Document 타입인데, JS와 다르게 TS에서 정의된 대로 사용하다보니, 객체와 같이 사용하려면 toObject()를 통해 객체로 변환해서 사용해야 했다.

DB 스키마 정의

이번 프로젝트에서 Post는 2가지 종류가 있다.
모임 게시글, 일반 게시글 2가지로 나뉜다.

DB를 정의할 때 2가지 방법을 생각했다.

  1. 각각 정의 한다.
  2. 하나로 정의하고 모임 게시글의 경우 추가적인 컬럼을 둔다.

각각 정의할 경우 겹치는 부분이 많고,
하나로 정의할 경우 Optional로 처리되는 부분이 많았다.

다른 부분보다 겹치는 부분이 더 많다고 판단하고
하나의 스키마로 정의해 모임글에 필요한 내용을 옵션으로 처리했다.

로거

1차 플젝에선 winston, morgan을 이용해 로거를 구성했다.
하지만 짧은 시간 구성하는 프로젝트에서 구성해야하는 보일러 플레이트가 크다고 판단했다.

pino라는 로깅 라이브러리가 있는데, 짧은 코드로 가볍게 사용할 수 있었다.

Google OAuth & JWT

1차 플젝에선 로그인 관련 로직을 담당하지 않았는데, 2차에선 로그인 관련 로직을 구현해야 했다.

왜 Google 로그인?

로그인 방식을 선택함에 있어서도 로컬, Google, 혹은 다른 OAuth 방식들이 존재했다. 왜 구글 로그인을 선택했을까?

  1. 로컬로 유저정보를 저장하는 것은 보안 상 준비해야할 것이 많다. 짧은 개발 시간에 맞지 않다고 판단했다.
  2. 엘리스 OAuth가 가능하다면 사용했겠지만, 없다면 Google이 개발자라면 누구나 가지고 있는, 쉽게 로그인할 수 있는 방법이라고 생각했다.

JWT 보안

토큰이 만료기한이 끝나기 전에 탈취 당하는 경우 계정 권한에 접근하는 보안 문제가 발생한다.

Token의 만료기한을 짧게 할까?
하지만, 만료기한이 짧으면 로그인 주기가 짧아진다.

구글에 검색해보니 Access Token, Refresh Token을 사용한다.
만료기한이 짧은 Access Token을 발급하고, Access Token이 만료되면 Refresh Token을 확인하고 다시 발급한다. Refresh Token도 만료된 경우 다시 로그인하게 된다.

Google OAuth

구글 로그인으로 유저 기능을 구현하는데, 최초 로그인으로 구글 정보를 가져오고 이후 로그인 상황에서 로컬 로그인으로 진행되어야 하는지 궁금했다.

다른 사이트를 살펴본 결과 구글 로그인이 구현된 경우 로그인 할 때마다 구글에 요청을 보내는 것을 확인했다. 매번 요청을 보내고 유저가 있는 경우 유저 정보를 가져오도록 했다.

댓글, 대댓글 구현하기

RDBS에서 댓글, 대댓글을 구현한다면 댓글 도메인을 정의하고 관계를 정의해 구현했을 것이다.

하지만 MongoDB는 이와 다르게, Post 아래 Comment 스키마의 배열을 정의해 저장했고, Comment 스키마 아래 Reply 스키마 배열을 정의해 하나의 도큐먼트로 불러올 수 있도록 구현했다.

Post comments는 Array<Comment>
Comment replies는 Array<Reply>

테스트 코드 오류

E11000 duplicate key error collection: coderland.posts index: gathering.members.googleId_1 dup key

테스트 코드를 진행하면서 에러가 발생하기도, 발생하지 않기도 하는 문제가 있었다. 에러 코드는 위와 같았다.

에러 코드 내용을 찾아보니, E11000 에러코드는 새로운 데이터를 저장하려고 할 때, 중복에 대한 에러였다.

테스트 케이스를 반복할 때, 데이터를 지우고 다시 쓰고 했으나 병렬적으로 이루어지면서 겹치는 것 같았다.

User 스키마를 확인해보니

googleId: {
  unique: true
}

googleId의 값을 유니크한 값으로 설정했는데, 해당 코드가 Post 전체를 호출할 때도 유저 값은 항상 유니크하게 가져오려고 하는 문제 같았다.

유저 중복 가입을 막으려고 했던 옵션이었는데, 생성할 때만 아니라 사용할 때도 Unique 옵션이 적용되는 것 같았다. 생각하는 것과 다르게 작동해 삭제했고, 정상적으로 진행되었다.

Post 조회수

유저가 Post를 조회할 때, Post에 대한 조회수를 어떻게 구현할 것인가? 가장 쉽게 생각한 방법은 API가 호출될 때마다 조회수를 높이는 것이었다. 하지만, 해당 방법은 새로고침을 연속으로 하거나, 반복 클릭으로 조회수를 계속 올릴 수 있었다. 쉬운 방법이 가장 좋은 방법은 아니었다.

  1. 쿠키를 이용해 시간 내에 조회수 올리는 방법

    Request 객체에서 쿠키 정보를 가져와 쿠키 안에 특정 필드에 조회한 Post 정보를 확인하는 방법으로 조회수를 올릴 지, 말 지 정하는 방식이다.

  2. 한 유저는 한 포스트에 한 번의 조회수를 올리도록 구현하는 방법

    Post에 views 배열을 저장해 유저 정보를 저장한다. 해당 Post를 조회한 유저는 views 배열에 유니크한 값으로 저장된다.

팀원과 회의 결과 한 유저가 한 포스트에 한 번의 조회수가 좋다라는 의견으로 2번을 선택했다. 무한정 커지는 서비스라면 Post에 저장되는 값이 많아지겠지만, 엘리스 수료생들을 한정으로 운영된다면 인원이 정해져있어 충분하다고 판단했다.

엘리스 수료생 인증

엘리스 수료생들의 커뮤니티를 제작하면서 가장 중요한 것은 엘리스 수료생을 어떻게 인증할 것인가에 대한 문제였다.

  1. 엘리스 수료증 이미지 분석을 통한 인증

    엘리스에서 발급받은 수료증을 통해 인증하는 방식을 생각했을 때, 수료증을 받는 기간, 이미지 분석에 대한 신뢰성, 수료증만 있다면 공유될 수 있을 가능성 등의 문제로 선택하지 않았다. 한가지 예시로 깃헙 프로를 위한 학생증 인증의 경우도 필체로 써진 학교 이름, 만료 기간을 통해서도 인증이 가능했기 때문에 신뢰성의 문제가 있다고 판단했다.

  2. 엘리스에서 명단 발급

    엘리스에서 수료생 명단을 받아 수료생 명단에 있는 경우 수락하는 방법이었는데, 개인 정보를 발급하는 것이 어렵다고 판단했다.

  3. 엘리스 프라이빗 깃랩

    엘리스에서는 프라이빗 깃랩을 사용했다. 권한이 있는 경우에만 깃랩에 접근할 수 있기 때문에, 엘리스를 수료하거나 엘리스에서 코칭을 진행한 경우에 해당 계정을 가질 수 있었고, 이에 엘리스 커뮤니티에 속할 수 있다는 것을 인증할 수 있다고 판단했다.

깃랩을 활용해 깃랩 페이지에 인증키를 넣고 저장하면 해당 유저와 인증키를 확인해 인증하는 로직으로 회원 인증을 구현했다.

async checkUserAuthKey(userId: string, username: string) {
  const gitlabUrl = `http://${username}.kdt-gitlab.elice.io/auth/`;
  const authKey = await this.UserModel.getUserAuthKey(userId);

  let result: AxiosResponse;
  try {
    result = await axios.get(gitlabUrl);
  } catch (error) {
    throw new Error("유저 ID를 확인해주세요!");
  }
  if (!String(result.data).includes(authKey)) {
    throw new Error("인증키를 확인해주세요!");
  }
  await this.UserModel.updateGrade(userId);
}

유저 등급 관리

수료생 인증이 구현되고, 인증이 되었는지에 대한 필터가 필요했다.

해당 문제를 미들웨어에서 User 정보를 확인해 Grade에 따라 접근을 제어했다.

회원 탈퇴 문제

회원 탈퇴의 경우 회원의 정보를 삭제한다 혹은 비활성화 한다로 고민했다.

팀원들과의 회의에서 회원 탈퇴의 경우 비활성화시키는 것이 맞다고 판단해 Grade를 -1로 하는 방법으로 진행했다.

하지만 이것이 회원 탈퇴라는 의미와 맞는가 고민되었다. (휴면이 아닌지)

profile
주니어 개발자입니다. 🚀

0개의 댓글