항해 4주차 회고

Yeon Jeffrey Seo·2021년 10월 10일
0

항해🚢

목록 보기
6/16

3주차 프로젝트 - 게시판 만들기

1. 기술 스택

- Language
    Javascript ES6
    
- Front-End
    HTML5
    CSS
    ejs 3.1.6
    Bootstrap 5.1.2
    
- Back-End
    Node.js 16.9.1
    express 4.17.1
    mongoDB 5.0.2
    mongoose 6.0.7
    Joi 17.4.2
    
 - ETC
    Jest 7.2.4
    Insomnia 2021.5.3
    Git 2.32.0 / Github
    draw.io 15.4.0

2. 기능

기능을 어떻게 분류하면 좋을까 고민했다. 지금은 백엔드를 다루고 있으니, 결국 내가 다루는 리소스를 기준으로 분류를 해 봤다.

  1. 회원(User)
  • 회원가입

    • 입력값 : username, email, password, comfirmPassword
    • 중복 확인 : User collection에서 username, email을 기준으로 조회하여 중복되는 값이 하나라도 있으면 오류 반환 (status 400)
    • password, confirmPassword 일치 여부 확인 : 두 문자열을 비교하여 일치하지 않을 경우 오류 반환 (status 400)
    • Joi를 활용한 입력값 검증 : 정규 표현식과 Joi Schema를 활용하여 입력값 검증.
    • 암호화 : bcrypt 사용
  • 로그인

    • 입력값 : email, password
    • 회원 조회 : User Collection에서 email을 이용하여 해당 회원 조회. 없을 경우 에러 반환(status 400)
    • 비밀번호 비교 : bcrypt 사용해서 password, hashedPassword 비교
    • 토큰 생성 후 전송 : jwt 방식을 활용하여 토큰 생성, 데이터는 조회 결과의 _id 저장. 응답에 토큰 적재 후 전송
    • 토큰 저장 : 프론트엔드에서는 응답의 토큰을 localStorage에 저장
  • 로그인 검사

    • 데이터 : localStorage에서 토큰
    • 토큰 검증 : token, key string을 활용하여 토큰 검증
    • 사용자 조회 후 전송 : token에 저장된 user _id로 사용자 조회 후 데이터 전달
  1. 게시물
  • 게시물 생성
    • 입력값 : title, text, password, user(로그인 검사 미들웨어에서 넘겨받은 값)
    • 게시물 생성 : 입력값을 사용해서 게시물 생성
  • 전체 게시물 조회
    • 메인 페이지 렌더 후, 프론트엔드 자바스크립트로 전체 게시물 조회 API 호출
    • 백엔드에서는, 조회 결과를 작성일 기준 최신순으로 재정렬 후 전송.
  • 특정 게시물 조회
    • 특정 게시물 클릭시, 해당 게시물의 id를 path parameter로 받아, 게시물 조회 후, 상세 페이지 서버사이드 렌더 시 함께 전송
  • 특정 게시물 수정 / 삭제
    • 사용자 - 작성자 비교 : 현재 localStorage에 저장된 토큰과, 게시물의 author를 이용하여 본인이 작성한 댓글인지 확인
    • 수정 / 삭제 : 수정 시, 입력값을 받아 DB 업데이트. 삭제 시, DB에서 해당 게시물 삭제
  1. 댓글
  • 댓글 작성
    • 특정 게시물에 대한 댓글 : 댓글은 게시물에 종속되어 있음. 댓글 작성시, 해당 게시물을 path parameter로 전송받음.
    • 입력값 : text
    • 댓글 생성 : 종속하는 게시물의 _id, 댓글 작성자(현재 로그인 중인 사람), text를 이용해서 DB에 댓글 생성
  • 댓글 조회
    • 게시물 상세 페이지 이동시, 해당 게시물에 대한 댓글 전체 조회
  • 댓글 수정 / 삭제
    • 사용자 - 작성자 비교 : 현재 localStorage에 저장된 토큰과, 댓글의 author를 이용하여 본인이 작성한 댓글인지 확인
    • 수정 / 삭제 : 수정 시, 입력값을 받아 DB 업데이트. 삭제 시, DB에서 해당 댓글 삭제

3. Router 구성

  1. Root router
app.use("/", renderRouter);
app.use("/api/users", usersRouter);
app.use("/api/postings", postingsRouter);
  1. Render router
rootRouter.get("/", home);
rootRouter.get("/postings", getPostings);
rootRouter.get("/postings/:id/detail", getDetail);
rootRouter.get("/postings/:id/edit", getEdit);
rootRouter.get("/signup", getSignup);
rootRouter.get("/login", getLogin);
  1. User router
userRouter.post("/", validateSignUp, postSignup);
userRouter.post("/auth", postAuth);
// 페이지 별로 로그인 검사를 위해 별도 API 준비
userRouter.get("/me", authMiddleware, getMe);
  1. Posting router
postingRouter
  .route("/")
  .get(readAllPostings)
  .post(authMiddleware, postPostings);

postingRouter
  .route("/:id")
  .patch(authMiddleware, patchPostings)
  .delete(authMiddleware, deletePostings);

postingRouter
  .route("/:id/comments")
  .post(authMiddleware, postComment)
  .get(authMiddleware, getComments)
  .delete(authMiddleware, deleteComment)
  .patch(authMiddleware, patchComment);

4. Schema

  1. User
const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  email: { type: String, required: true },
  password: { type: String, required: true },
});
  1. Posting
const postingSchema = new mongoose.Schema({
  title: { type: String, required: true },
  author: { type: String, required: true },
  createdAt: { type: Date, default: Date.now, required: true },
  password: { type: String, required: true },
  text: { type: String, required: true },
});
  1. Comment
const commentSchema = new mongoose.Schema({
  ownedPosting: { type: mongoose.Types.ObjectId, required: true },
  author: { type: String, required: true },
  text: { type: String, required: true },
  createdAt: { type: Date, default: Date.now, required: true },
});

5. 프로젝트 진행 중 고민 / 문제

  1. Router 구성
  • RESTful한 API를 작성하기 위해 고민을 좀 했다. url에는 자원을 표현하고, 특정 자원을 찾을 때는 path parameter를, 자원의 필터, 정렬은 query string을 사용하기로 했다.
  • 애석하게 이번 프로젝트는 query string을 사용할 일이 없어 사용하지 않았다.
  • 댓글은 게시물에 종속된다는 것을 url로 표현했다.
  • ex) /api/postings/:id/comments
  • 잘 한건지 모르겠다?!
  1. DB 관계 표현
  • 나는 초기에 user schema에도 posting와 comment에 대한 참조를 하여 user만 조회해도 게시물과 댓글을 알 수 있게 만들었다.
  • posting에도 user와 comment에 대한 참조를 했고, 마찬가지로 comment에도 user와 posting에 대한 참조를 했다.
  • 결과적으로 세 개의 모델이 서로를 참조하는 구조가 되었고, 이는 CRUD를 하면서 바로 문제로 이어졌다.
  • 하나의 모델에 대해 CRUD를 하면 다른 두 개의 모델 또한 CRUD를 수행해야 하는 상황이 발생해서 코드가 굉장히 복잡해졌다.
  • 만약 RDB를 사용해서 cascade CRUD를 수행했더라도, 하나의 모델이 다른 모델들에 영향을 끼쳤을 것 같다.
  • 따라서 user <-> commnet, posting 모델은 참조 관계가 없도록 분리하였다. 반면 comment는 posting에 종속되어야 한다. 그리고 comment가 어떤 posting에 종속되어야 하는지 알 수 있는 식별자가 필요했다. 필요에 의해 comment는 posting을 참조하도록 만들었다.
  1. Jest ES6 사용
  • Jest는 ES6 import 방법으로 사용할 수 없었다.
  • 다행히 Jest 공식문서에 해결 방법이 적혀있었다.

    https://jestjs.io/docs/ecmascript-modules

  • package.json에서 명령어를 바꿔 jest를 실행할 수 있도록 했다.
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  1. 테스트 코드 작성 수준 결정
  • 정보처리기사 필기 공부를 하면서 글로만 접했던 테스트 코드를 직접 작성해보았다. 문제는 어느 수준까지 테스트 케이스를 작성을 해야하는지였다. 테스트 케이스는 정말 작성하기 나름이라는 것을 깨달으면서 동시에 막막함이 몰려왔다. 하나의 정규 표현식에 대해서도 많은 테스트 케이스를 작성할 수 있었다.
  1. 응답 코드
  • 모든 API에 대해 정말 단순하게 응답을 지정했다. 좋은 건 200, 나쁜건 400. 결과의 의미에 대응하는 응답이 존재한다는 건 알고 있었지만, 하나 하나 찾아보기가 너무 귀찮았다.
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
    이번 프로젝트는 끝났지만 지속적으로 수정해야겠다.

6. Keywords

Middleware

  • 운영 체제와 응용 소프트웨어의 사이에서 작동하는 소프트웨어이며 소프트웨어간 데이터를 주고 받을 수 있도록 중간에서 매개 역할을 하는 소프트웨어이다.
  • 3계층 클라이언트/서버 구조 (3 Tier Architecture) 에서 미들웨어가 존재한다. 3 Tire Architecture에서는 Client Tier, Application Tier, Data Tire가 존재하는데, 이 중 Application Tier를 미들웨어(혹은 백엔드)라 한다.

Jest, Application Test
Jest?

  • Jest는 페이스북에서 만든 테스팅 라이브러리이다.
  • 기본적인 사용 방법은 다음과 같다.
test("테스트 설명", () => {
  expect("검증 대상").toXxx("기대 결과");
});
  • toXxx 부분을 matcher라 부른다. Jest에서는 많은 Matcher 함수를 제공하고 있다. 이번 프로젝트에서는 toEqual만을 사용해보았다.

Application Test
애플리케이션 테스트는 테스트 기준, 관점에 따라 다양한 테스트 방법이 존재한다. 나는 Joi schema에 대한 검증을 진행하였으므로, 프로젝트 수행 단계에 따른 테스트 중 단위 테스트(Unit test)를 진행했다고 볼 수 있겠다.
Schema는 회원가입 form data를 받아서 입력값이 형식에 맞는지 확인하기 위한 것이다.

export const isSignUp = async (signUpForm) => {
  try {
    await signUpSchema.validateAsync(signUpForm);
    return true;
  } catch (error) {
    return false;
  }
};

toEqual matcher에서 true/false를 사용하기 위해 테스트 코드를 간단하게 작성했다. 입력 값이 schema 검증을 성공하면 true를 반환하며, 실패하면 false를 반환한다.
테스트 결과는 아래와 같다.

테스트 케이스를 작성하면서 스스로와 많이 타협했다.(...) 얼마나 정교하게 테스트 케이스를 짜냐에 따라 테스트 코드도 변경해야 할 것 같고, 테스트 케이스도 수 십 가지는 더 만들 수 있을 것 같다.

7. 회고

2주차부터 나사가 좀 빠져있었다. 게임하고 노느라 공부할 시간이 절대적으로 부족했다. 시간이 부족하다 보니 새로 배운 MySQL, sequelize를 적용할 수가 없었고, 또다시 mongoDB와 mongoose를 써서 프로젝트를 진행했다. 다음 주 부터는 정신 바짝 차리고 항해에 임해야겠다.
이번 팀에서는 코드 리뷰를 진행했다. 확실히 다른 사람의 코드에서 배울 점이 많았고, 내 코드에서 피드백을 받는 것도 도움이 된다고 느껴졌다.
3주차는 다른 크루원들의 질문을 많이 받았다. 질문에 답변을 해주면서, 자연스럽게 내가 알고 있는 개념이 다시 정리가 되었다. 동시에, 질문을 할 때에도 고민을 해보고, 신중히 질문을 하는 것이 좋다는 것을 다시 깨달았다. 고민 과정에서 문제가 해결되기도 하고, 좋은 질문이 가야 좋은 답변이 온다.

8. 참고 문헌

미들웨어
https://m.blog.naver.com/limoremo/220073573980
https://expressjs.com/ko/guide/using-middleware.html
https://12bme.tistory.com/289

Jest
https://www.daleseo.com/jest-basic/

9. Github 링크

https://github.com/yeonjeseo/bulletin-board

profile
The best time to plant a tree was twenty years ago. The second best time is now.

0개의 댓글