4주 프로젝트 회고 - queue overflow

project Queue Overflow

Queue Overflow 👉 https://www.capybara25.com

깃헙 레포 바로가기

이번 프로젝트는 클론 프로젝트로 진행하였다. 4주라는 기한이 있었고, 새롭게 타입스크립트를 도입하였기 때문에 공부하는 시간은 따로 빼면 실질적으로 개발하는 기간은 3주 정도 였다. 때문에 2주차 프로젝트보다 볼륨은 크되, 기획에 들어가는 시간을 최대한 줄여보고자 했다. 이 점을 종합해 클론 프로젝트로 의견이 모아졌다. 클론할 대상 사이트 또한 여러가지 후보가 있었다. 여기서 우리 팀은 적당히 알려져 있으면서, 오픈된 튜토리얼이 없고, 개발자들 사이에서 친숙한 사이트라는 점에서 스택 오버플로우가 적절하다고 판단했다.


프로젝트에서의 역할

백엔드 포지션을 맡았다. 개발에 들어가기 앞서 팀원들과 함께 와이어프레임을 만든 후 스키마를 짜고, 대략적인 api를 구성하여 초안을 잡았다. 그 후 개발하면서 필요에 따라 살을 붙이며 새로 추가하기도 했다. 이번 프로젝트에서는 새롭게 타입스크립트를 도입했다. 타입스크립트는 자바스크립트에 정적 타입 정의가 추가된 것으로, 개발 시 코드를 훨씬 수월하게 읽을 수 있다. 특히 복잡하고 긴 코드에서, 데이터 타입이 미리 정의되어 있기 때문에 코드 작성 시 에러 방지에 효율적이다. 또한 함수 정의 시에도 타입 지정이 가능하기 때문에, 함수를 사용할 때 보다 명확하게 인자에 대한 정보를 알 수 있어 편리한 개발이 가능하다. 타입스크립트를 프로젝트에 적용하기 위해 미리 일주일 정도 공부한 후 본격적인 개발에 들어가기로 했다.


신경썼던 부분

sequelize-typescript

  • 좀더 친숙한 클래스로 모델을 정의하기 위해 sequelize-typescript 라이브러리를 사용했다.
  • 아무래도 나온지 얼마 되지 않아 많은 예시가 없었다.
  • 특히 관계 설정 시 실제 컨트롤러에서 실행할 때 제대로 관계가 설정되지 않았는데, 에러메세지를 보면서 차근차근 풀어나가니 원하는 응답 객체를 받을 수 있었다.
  • 게다가 검색 기능을 위해 스코프를 정했어야 했는데, 여기에서 셀프 조인이 들어가야 했다. 이것 또한 에러 메세지를 통해 해결했고, 셀프 조인을 위해서는 id에 foreign key 지정을 해줘야 했다.
// models/Post.ts 예시
import { AllowNull, AutoIncrement, BelongsTo, BelongsToMany, Column, DataType, Default, ForeignKey, HasMany, HasOne, Model, NotEmpty, PrimaryKey, Table } from "sequelize-typescript"

import { User } from "./User";
import { Tag } from "./Tag";
import { PostTag } from "./PostTag";
import { Answer } from "./Answer";
import { Choose } from "../models/Choose";

export interface PostI {
  id?: number
  userId: number
  title: string
  body: string
  votes: number
  views: number
}

@Table({
  tableName: "posts",
  timestamps: true
})
export class Post extends Model implements PostI {
  
  @ForeignKey(() => Post)
  @AutoIncrement
  @PrimaryKey
  @Column
  id?: number

  @ForeignKey(() => User)
  @AllowNull(false)
  @NotEmpty
  @Column
  userId!: number

  @AllowNull(false)
  @NotEmpty
  @Column
  title!: string

  @AllowNull(false)
  @NotEmpty
  @Column(DataType.TEXT)
  body!: string

  @Default(0)
  @AllowNull(false)
  @NotEmpty
  @Column
  votes!: number

  @Default(0)
  @AllowNull(false)
  @NotEmpty
  @Column
  views!: number

  @HasOne(() => Post, { constraints: false })
  post!: Post[]

  @BelongsTo(() => User) 
  user!: User;

  @HasMany(() => Answer)
  answer!: Answer[];

  @BelongsToMany(() => Tag, () => PostTag)
  tag!: Tag[];

  @HasOne(() => Choose)
  chooseTable!: Choose[]
}


Pagination

  • 서버 단에서의 페이지네이션을 구현해야 했다.
  • sequelize 문법에서 offset 옵션과, limit 옵션에 대해 알아야 했다.
  • 클라이언트에서 각 페이지에 요구하는 limit이 달랐기 때문에, 큰 틀을 만들어두고 사용했다.
  if(query.page) {
    let pageNum: any = query.page;
    let offset: number = 0;
    if(pageNum > 1) {
      offset = 15 * (pageNum - 1);
    }
    const postByPage = await Post.findAll({
      // ..
      order: [["id", "DESC"]],
      offset,
      limit: 15
    })


nginx

  • 서버 보안과 성능을 올리기 위해 배포 서버에서 Nginx를 사용해주었다.
  • 사용자가 서버에 바로 접근하는 게 아닌, nginx가 중간 대리자 역할을 해주고, nginx 포트를 통해서만 접속할 수 있다.
  • 클라이언트로부터 요청이 들어오면 정적 파일을 다이렉트로 제공해주기 때문에 서버에 부담이 되지 않는다.
  • 클라이언트 요청을 분산하는 reverse proxy를 지원하고 있기 때문에 로드밸런서로 활용되어 서버의 부하를 줄일 수 있다.
  • 배포서버(AWS EC2)의 /etc/nginx/sites-enabled/default 에서 nginx를 설정해준다.
  • 2주 프로젝트때와 같은 서버를 사용 중이었기 때문에 도메인에 따라 라우팅을 해 줘야 했다.
server {
        listen 80;
        listen [::]:80;
        listen 443;
        listen [::]:443;
        server_name <첫번째 서버 도메인>;

        access_log /var/log/nginx/reverse-access.log;
        error_log /var/log/nginx/reverse-error.log;

        location / {
                    proxy_pass http://127.0.0.1:4000;
  }
}

server {
        listen 80;
        listen [::]:80;
        listen 443;
        listen [::]:443;
        server_name <두번째 서버 도메인>;

        access_log /var/log/nginx/reverse-access2.log;
        error_log /var/log/nginx/reverse-error2.log;

        location / {
                    proxy_pass http://127.0.0.1:4001;
  }
}


gzip

  • 추가적인 성능 향상을 위해 gzip 파일 압축 응용 소프트웨어를 사용해주었다.
  • 컨텐츠름 압축해주어 전송되는 데이터 양을 줄일 수 있다.
  • bzip2, lzip 등의 다른 압축 프로그램들에 비해 압축률이 높은 동시에 빠른 속도로 처리된다.
  • /etc/nginx/nginx.conf의 http 블록에서 설정할 수 있으며 여러 옵션들을 적용할 수 있다.
# Gzip Settings
##

gzip on;

gzip_min_length 500;
gzip_proxied any;
gzip_disable "msie6";
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_types text/plain text/css text/js text/xml text/javascript application/javascript application/x-javascript application/json application/xml application/rss+xml image/svg+xml image/png;gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
  • 옵션
    • gzip_comp_level: 숫자가 클수록 압축률이 올라가고 속도는 느려진다. 1에서 9 사이의 숫자로 설정.
    • gzip_min_length: 압축을 적용할 컨텐츠의 최소 사이즈
    • gzip_buffers: 버퍼의 숫자 크기
    • gzip_proxied: proxy에서 요청할 경우 압축 여부(any일 경우 항상 압축), 기본값은 off
    • gzip_types: 압축할 컨텐츠 유형 설정
  • 압축 설정이 제대로 되었는지 확인하기 위해서는 개발자 도구의 네트워크 탭에서 응답헤더를 확인하면 된다.


후기

시간 대비 효율을 위해 몇 가지 부분을 어드밴스드로 뺀 것이 조금 아쉬웠다. 클론 프로젝트인 만큼 대상 사이트를 완벽하게 모두 클론해야한다는 강박이 있었지만, 스택 오버플로우는 자세히 들여다보면 우리 생각보다 정말 거대한 규모의 사이트였다. 타입스크립트가 정말 자바스크립트에 타입만 더한 것이라는 것만 보면, 자바스크립트를 조금 쓰던 사람에겐 별 거 아닐 것이라고 생각하겠지만 실상은 이해할 수 없는 부분에서의 타입 오류, 자바스크립트로 컴파일 되는 데 걸리는 시간 등등 자잘한 방해 사항이 있어서 개발할 수 있는 양이 점점 줄어들기 시작했다. 특히나 내 할아버지 노트북으로는… 정말 힘들었다. 개발하는 데 어려움보다, 노트북 달래는 데 더 힘을 쓴 듯 하다.. 다른 팀들처럼 nestjs나 mongoDB 같은 새로운 스택을 도입했었으면 그래도 더 성장이 있었을까 하는 생각도 들지만, 그랬다면 프로젝트 규모는 훨씬 더 줄어들었을 것 같다는 생각도 들어서 아직 마음이 복잡하다. 그래도 배움의 시기는 언제가 됐든 늦는다고 생각하지 않는다. 프로젝트도 끝났으니 강의도 여러개 배우면서 스킬 스펙트럼을 더 넓혀보고 싶다. 솔직히 타입스크립트만 하나 추가됐다 뿐이지, 나머지는 평소 쓰던 것과 별 다를 게 없어서 단조로운 기분이었다. 새로운 걸 배우고 싶다.

Categories:

Project

Load Comments