INVESTING.COM 클론 코딩(feat. Vue JS SSR, CANVAS API)

파일럿 프로젝트
줌인터넷 서비스개발팀 프론트엔드 파트 주니어 개발자들(재민, 도경, 정훈)이 수습 기간 동안진행했던 파일럿 프로젝트 입니다. 진행된 프로젝트는 GitHub 레포지토리에서 확인하실 수 있습니다.

조회수

목차

  1. 프로젝트 개요
    1.1 프로젝트 주제
    1.2 요구사항
    1.3 프로젝트 기능소개

  2. 협업 방법

  3. 프로젝트 구조
    3.1 프로젝트 전체 구조
    3.2 패키지별 구조
    3.3 Backend 구조
    3.4 Frontend 구조
    3.5 common 구조

  4. 기술적 고민
    4.1 재민 님의 고민
    4.2 도경 님의 고민
    4.3 정훈 님의 고민

  5. 회고
    5.1 재민 님의 회고
    5.2 도경 님의 회고
    5.3 정훈 님의 회고

  6. 마치며

1. 프로젝트 개요

1.1. 프로젝트 주제

파일럿 프로젝트의 의의는 미리 실무를 위한 웹 서비스 기반 지식, 기술을 습득하여 신규 입사자들이 보다 빨리 실무에 적응할 수 있도록 하는 데 있습니다.

그리고 파일럿 프로젝트가 끝나면 바로 줌닷컴 신규 서비스 개발 프로젝트에 투입될 예정이었습니다. 미리 도메인에 대해 학습케하여 더욱 실무 적응을 돕고 프로젝트를 성공적으로 수행할 수 있게끔 하고자 했습니다.

두 마리 토끼를 잡을 수 있도록 주제를 선정해야 했습니다.

그래서 ..

INVESTING.COM 클론코딩

1 모바일 INVESTING.COM

앞으로 진행될 프로젝트에선 주식, 원자재, 가상화폐 등의 자산들을 폭넓게 다룰 예정입니다. 많은 투자 지표들을 상세히 다루고 캔들 차트, 라인 차트 등 풍부한 그래픽 자료를 제공해야 합니다.

그러한 컨텐츠를 이미 제공하고 있는 INVESTING.COM 이 있었습니다. INVESTING.COM 은 전세계 거래소 250곳의 실시간 데이터, 관련 뉴스 및 분석을 44개의 언어로 제공하는 금융시장 플랫폼입니다. 월간 2,100만 명의 이용자를 자랑합니다. 투자자들이 한 번에 필요한 정보를 모두 확인할 수 있는 장소를 제공하는 것을 목표로 하고 있습니다.

이러한 서비스를 클론 코딩하면서 기술적, 도메인적 이슈를 해결하는 경험, 실무에 필요한 지식 및 기술을 얻을 수 있으리라 생각했습니다.

이러한 이유로 INVESTING.COM 을 클론 코딩 하기로 했습니다.

개발은 5. 10 ~ 6. 24 까지 약 7주간 이루어졌습니다.

1.2. 요구사항

1.2.1 기술적 요구사항

1.2.2 기능적 요구 사항

1.3. 프로젝트 기능 소개

2 업무 분장표 우선 간단하게 태스크들을 정의하고 분배하였습니다. 무엇을 해야 할지 인지한 상태에서 개발을 시작할 수 있었습니다.

검색, 관심목록은 저렇게 나눌 땐 몰랐는데 양이 만만치 않았습니다…

크게 공통 개발 기능, 개별 개발 기능 두 가지로 나누어 개발하였습니다.

공통 개발 기능은 모두가 한번씩 구현해보기로 했습니다. 전체 UI 구성과 로그인은 실무 내내 다룰 것입니다. 앞으로 많은 실무 프로젝트를 수행하기 위해선 차트에 대한 이해가 필수적입니다. 그래서 이 세 부분을 공통 개발 기능에 추가했습니다.

개별 개발 기능은 각자가 전담하여 자신의 코딩 스타일, 역량을 잘 드러내도록 개발하였습니다. 신규 입사자들을 더 파악하여 팀에 잘 녹아들 수 있도록 도움을 주기 위해서입니다.

1.3.1 공통 개발 기능

Google 및 자동 로그인

3 재민 님 프로젝트

이메일 회원가입 및 로그인

4 재민 님 프로젝트

마켓 페이지

5 도경 님 프로젝트

6 정훈 님 프로젝트

다크모드 지원 & 스와이프

7 재민 님 프로젝트

비동기 처리 UI(Loading, Error 등)

8 정훈 님 프로젝트

9 재민 님 프로젝트

10 재민 님 프로젝트

차트

11 도경 님 프로젝트

12 재민 님 프로젝트

13 정훈 님 프로젝트

1.3.2 개별 개발 기능

자산종목 상세 페이지

14 재민 님 프로젝트

15 재민 님 프로젝트

관심목록 페이지

16 재민 님 프로젝트

종목검색 페이지

17 재민 님 프로젝트

댓글 기능

18 정훈 님 프로젝트

19 정훈 님 프로젝트

뉴스, 분석 리스트 및 상세 페이지

20 도경 님 프로젝트

21 도경 님 프로젝트

더보기 페이지

22 도경 님 프로젝트

2. 협업 방법

이번 파일럿 프로젝트는 3명이 같이 진행했기 때문에 협업을 하는 방법도 중요했습니다. 어떤 방식과 도구를 사용해서 협업을 진행했는지 소개하고자 합니다.

스크럼

매일 업무를 시작하기 전에 짧은 스크럼을 하여 자신이 했던 업무와 어려운 점, 문제점 등을 공유하고 의견을 나누는 시간을 가졌습니다. 계속해서 서로 하는 일들을 체크하고 조율할 수 있어서 프로젝트를 더욱 원활하게 진행할 수 있었습니다.

문서 공유

프로젝트 진행 중 서로 공유할 사항이 생기면 문서로 기록해두었습니다. 문서를 작성하면서 작성하는 사람도 한 번 더 내용을 정리할 수 있었고, 타입을 나눠서 보는 사람들도 나중에 편하게 볼 수 있었습니다.

23 작성한 스터디, 참고자료 문서들

모노레포

서로 비슷하지만, 개별적인 서비스를 개발하므로 마이크로서비스 아키텍처를 적용하기로 했습니다. 마이크로서비스 아키텍처에서는 각각의 서비스들이 독립적이므로 따로 사용하는 패키지를 관리하기 쉬웠고, 개발과정에서 충돌도 더 적었습니다.

그리고 마이크로서비스 아키텍처를 구성하면서 여러 서비스들을 하나의 레포지토리에서 관리할 수 있는 모노레포를 같이 적용했습니다. yarn workspaces로 쉽게 설정할 수 있었는데요, 여러 레포지토리로 구성을 하면 서비스 간의 공통코드를 사용하기가 불편한데 모노레포에서는 이런 점을 쉽게 할 수 있었습니다.

24 모노레포의 구조

코드리뷰

이번 프로젝트를 하면서 새로운 기술 스택을 사용하고 서로 기술에 대한 숙련도의 정도도 달랐기 때문에 코드 품질을 비슷하게 맞추기 위해 코드 리뷰를 활발하게 진행했습니다. 다양한 관점에서 피드백을 주고받으면서 혼자 했을 때보다 더 빠르게 성장할 수 있었습니다.

3. 프로젝트 구조

25

3.1. 프로젝트 전체 구조

앞서 협업 관련해서 모노레포에 대해 설명 드렸는데요,

프로젝트 루트에는 패키지들이 저장된 node_modules 와 각자가 구현한 패키지들이 있는packages , 그리고 각종 설정 파일들로 구성되어 있습니다.

코드 컨벤션을 최대한 편하게 맞출 수 있도록 eslint, prettier를 설정했고, VSCode extension인 vetur에 대한 설정도 추가했습니다.


3.2 패키지별 구조

본격적인 구현물은 packages 안에 있는데요, 각자의 이름을 걸고(…?) 구현한 패키지들과 공용으로 사용할 common 패키지로 구성되어 있습니다.

3.2.1 개별 서비스 패키지

팀 프로젝트이기 때문에 큰 틀은 유사하게 가져가지만, 신입사원 파일럿 프로젝트인 만큼 각자의 코드에는 각자의 개성이 드러났으면 좋겠다는 요구사항이 있었습니다. 이러한 개성은 파일 구조를 구성하는 데에서도 볼 수 있었습니다.

26 27 28 도경 님, 재민 님, 정훈 님 순입니다.

크게 backend - frontend 로 나누어 서버단에서 필요한 작업은 backend에서, 클라이언트에서 필요한 작업은 frontend에서 담당하도록 했습니다. domain 으로 된 부분은 axios 응답 데이터처럼 backend-frontend에서 공통으로 사용될 수 있는 인터페이스를 모아 두었습니다.

29 30

도경 님과 정훈 님은 DB 사용을 위해 docker를 사용했는데요, 도경 님은 docker 관련 설정을 별도로 분리하고 나머지 프로젝트 로직에 대한 부분은 src 폴더로 모은 반면, 정훈 님은 그냥 wiii package 루트에 docker-compose.yml 만 넣었습니다. 정훈 님 패키지에는 __tests__ 폴더도 보이긴 하는데요, 흔적기관 같은 거라 보시면 될 것 같아요…


3.3 Backend 구조

backend 에는 여느 백엔드 프로젝트처럼 controller - service - db(repository - model) 로 기능을 분리했습니다. API Key 등 설정 관련 상수는 config에 모았습니다. 도경 님은 특별히 middlewares를 추가해서, 사용자 인증 관련 미들웨어를 구현했네요.

31 32

DB는 저희 모두 MongoDB를 사용했는데, ORM으로 도경, 재민 님은 Mongoose를, 정훈 님은 TypeORM을 사용했습니다. 두 분은 model에서 필요한 schema만 작성하고 해당 schema를 이용하는 로직은 각 Service에서 mongoose 기본 API를 활용하는 방식으로 구현했습니다.

TypeORM에서도 당연히 find, create 등 기본적인 CRUD API는 제공하는데요, custom repository를 만들어서 추가적인 로직을 구현할 수 있더라구요. 그래서 TypeORM 개념을 따라서 entity - repository를 구분해 DB 관련한 로직을 구현했습니다. (끝나고 보니 entity는 주로 RDBMS 테이블에서 사용하는 개념이라 MongoDB에서 사용하는 게 맞는지 잘 모르겠네요.. 디알못이라..)


3.4 Frontend 구조

저희가 프론트엔드 개발자들이고, 프로젝트 또한 프론트엔드 작업이 중요한 프로젝트이기 때문에 프론트엔드 구조에 대해서는 좀 더 자세히 설명해 드리는 것이 좋을 것 같습니다.

33 34 35 도경 님, 재민 님, 정훈 님 순입니다.

프론트엔드 역시 큰 구조는 유사합니다. Vue 컴포넌트가 들어가는 components, 각 페이지를 구성하는 views, Vuex 사용을 위한 stores, Vue-router 사용을 위한 router, 전역에서 사용할 SCSS 변수, 클래스를 위한 styles, Axios 등 서버와의 통신 기능을 담당하는 service/api 등 프로젝트에 필수적인 부분들은 유사하게 구조를 잡았습니다.

components 디렉토리는 공통으로 사용하고 있지만, 내부 구성은 조금씩 다르게 구현했습니다. 도경 님과 재민 님은 기능별로 컴포넌트 단위를 나누어 구현했습니다. 도경 님은 Vue의 Mixin 컴포넌트도 적절히 사용하셨네요. 정훈 님은 아토믹 디자인 패턴을 적용했는데요, 초반엔 컴포넌트들을 잘게 쪼개는 과정이 필요해서 구현 시간이 좀 더 필요했습니다. 하지만 뒤로 갈수록 높은 재사용성 덕분에 필요한 컴포넌트들을 불러와서 갖다 붙이기만 하면 페이지 하나가 뚝딱뚝딱 만들어지는, 재미있는 경험을 할 수 있었습니다. 여기에 props를 좀더 잘 활용하고, Mixin도 함께 쓴다면 재사용성이 훨씬 더 높아질 것 같아요.

36

37

캔버스 차트를 그리는 로직도 서로 다른데요, 도경 님은 chart 디렉토리를 별도로 만들고 내부 로직을 클래스로 구현했습니다. 반면 재민 님과 정훈 님은 함수를 활용해 구현했습니다. 재민 님은 Chart.vue 컴포넌트 내부 메서드로 구현했고, 정훈 님은 utils/chart 에서 함수로 구현했습니다.


38

3.5 common 구조

common 패키지는 개별 서비스 패키지 구조와 유사하지만, 모두가 공통으로 사용할 컴포넌트와 서비스 로직을 모아둔 패키지입니다. 업무 분배를 통해 담당자가 뉴스, 댓글 등 기능과 컴포넌트를 구현하고, 각자의 서비스에서 마치 node_modules의 패키지를 불러와서 사용하는 것처럼 담당 기능을 사용할 수 있도록 했습니다.

별도의 서비스가 아니기 때문에 resource 등 불필요한 부분과 controller 처럼 각자 구현할 수밖에 없는 부분은 제외했습니다. backend - frontend를 동시에 활용해야 하는 기능을 공용으로 구현하다 보니 코드가 조금 지저분해지는 것은 아쉽긴 하지만 어쩔 수 없었던 것 같아요.


프로젝트 전반에 대한 소개는 이 정도로 마무리하고, 각자가 구현하면서 고민했던 내용들에 대해 말씀드리겠습니다.

4. 기술적 고민

4.1 재민 님의 고민

4.1.1 실시간성 보장을 위한 기술 선택 고민

사용자 편의를 위해 마켓 페이지와 종목 상세페이지 내의 가격 정보 변화를 실시간으로 제공해야 했습니다.

이렇게 외부 정보를 받아 동적으로 반영하기 위한 최선의 방법이 무엇일지 고민했습니다.

4.1.2 여러 선택지

WebSocket

Regular Polling

SSE(Server-Sent-Event)

4.1.3 결국 Long Polling

Long Polling 이란?

39 출처 : Modern Javascript Tutorials

코드

40 클라이언트에서 계속 재귀적으로 서버로 주식 관련 데이터를 요청하는 코드

41 주식 관련 데이터를 가져오는 storeaction

4.2 도경 님의 고민

협업을 위해 코드리뷰를 진행하면서 몇 가지 고민들이 생겼습니다.

중요한 리뷰에 집중하기

코딩 컨벤션, 린트, 포맷팅, 오타 등 비교적 가벼운 부분들에서 코드 리뷰를 하느라 시간을 많이 쓰는 경향이 있었습니다. 그렇게 되면 중요한 부분에서 리뷰를 놓칠 수 있는 문제가 있습니다. 이를 최소화하기 위해 husky와 같은 도구를 이용해서 커밋, 푸시 훅에서 체크할 수 있는 부분들을 먼저 체크하는 방법이 있습니다. 코드 리뷰 시에는 더 중요한 부분에 집중할 수 있기 때문에 다음 프로젝트부터 적용할 계획입니다.

작은 PR 만들기

PR의 코드가 적으면 코드 내의 이슈를 찾기가 쉽습니다. 하지만 PR이 커지게 되면 이슈를 찾기가 어렵고 코드가 괜찮아 보이는 문제가 생기는데요, 그렇기 때문에 PR은 최대한 작게 나누는 것이 좋습니다. 이를 위해 도움이 될만한 것들이 있습니다.

  1. 할 일을 명확하게 정하고 측정하기

    작업을 하기 전에 하는 일에 대한 명세를 미리 작성해두면 작업량에 대한 예상과 측정이 쉬워져 미리 PR을 어떻게 나눌지 생각할 수 있습니다. 깃허브의 이슈 또는 자라의 티켓이 많이 사용됩니다.

  2. 브랜치를 잘 나누기

    기능 브랜치를 여러 개의 서브 브랜치로 나눠서 작업한 다음 기존 기능 브랜치로 PR를 만들게 되면 작은 PR을 만드는 데 도움이 됩니다.

4.3 정훈 님의 고민

Server Sent Events 적용과 실패 그 사이..

종목별 상세페이지에 있는 차트 컴포넌트를 동적으로 만들어보고자 Server Sent Events(이하 SSE)를 적용해보았습니다.

SSE는 무엇인가

SSE는 아래 도표를 보고 설명을 보시면 좀 더 이해하기 쉬우실 것 같아요

42 출처: https://subscription.packtpub.com/book/web_development/9781782166320/6/ch06lvl1sec43/listening-for-server-sent-events

간단히 설명해 드리면,

일반적인 요청-응답과 동일하게 HTTP/HTTPS 프로토콜을 사용하지만, 클라이언트에서 한번 요청한 이후 keep-alive로 연결을 유지하면서 서버에서 계속해서 데이터를 보내는 기술입니다.

양방향 통신으로 지속적인 연결을 하는 Socket 과는 달리 단방향 통신을 위해 사용되기 때문에, (공식적인 확인은 할 수 없었지만) 트위터, 페이스북의 알림 기능에서도 사용되고, 저희 프로젝트처럼 주식 시세 정보를 실시간으로 보내는 데도 사용될 수 있습니다.

Socket에 비해 자주 사용되는 기술은 아니지만, 웹 표준 API(https://html.spec.whatwg.org/multipage/server-sent-events.html)기 때문에 모던 브라우저에서는 모두 사용할 수 있어요.

Socket이 아닌 SSE를 적용한 이유

참고로 했던 타 서비스들 상당수는 시세 관련 데이터를 실시간으로 전송하기 위해 Socket을 사용하고 있습니다. 저도 처음엔 Socket 쓰는 게 당연하다 생각했죠.

그런데 서버 단에서는 클라이언트에서 연결을 종료할 때까지 지속적으로 응답을 보내기만 하면 되기 때문에, Socket에 비해 상대적으로 구현하기 쉬웠고, 단방향 통신만 필요한 상황이어서 SSE를 적용하는 것이 괜찮을 것 같다고 판단을 했습니다.

SSE 적용 결과

SSE는 기존 로직과 크게 다르지 않게 구성할 수 있습니다.

먼저 서버 단에서는 controller에서 response.write 를 통해 데이터를 추가로 보내는 로직과 response.end 를 통해 클라이언트와의 통신이 끊어질 때 로직만 추가해서 구현하면 됩니다. 데이터 객체를 만드는 서비스 로직은 기존에 사용하던 로직 그대로 사용하면 됩니다.

SSE 관련된 서버 단 코드 전부를 아래와 같이 가져왔는데요, SSE 클래스 를 만드는 부분 때문에 생소해 보일 순 있지만, 응답 헤더를 지정하고 response.write를 조금 더 추상화한 것에 불과합니다.

setInterval을 사용하는 부분이 있는데, 완전한 실시간 시세 데이터가 아니라 일정 시간 간격으로 캐싱 된 데이터를 보내주는 방식으로 구현했기 때문입니다. 만약 파일럿 프로젝트가 아니라 실 서비스라면 interval이 아니라 현재가가 변동될 때마다 해당 데이터를 보내주는 방식으로 조금 바꾸면 될 것 같아요.


/**
 * @see https://github.com/zuminternet/investing-app-clone/blob/main/packages/wiii/backend/controller/SSE.ts
 */
export default class SSE {
  res: Response<any, Record<string, any>>;
  options: any;
  service: MarketService;

  constructor(res: Response, options) {
    this.res = res;
    this.options = options;
    this.setHeader();
  }

  private setHeader() {
    this.res.writeHead(200, {
      /** EventSource 활용 위한 연결 지속 */
      Connection: 'keep-alive',
      /** EventSource 활용 위한 text타입 전송 */
      'Content-Type': 'text/event-stream',
      /** 지정 초(15s)까지 public caching 허용 */
      'Cache-Control': `public, max-age=${times.sse}`,
      /** CORS 허용 */
      'Access-Control-Allow-Origin': 'http://localhost:3000',
    });
  }

  public write(data) {
    this.res.write(`data: ${JSON.stringify(data)}\n\n`);
  }
}

/**
 * @see https://github.com/zuminternet/investing-app-clone/blob/main/packages/wiii/backend/controller/Market.controller.ts
 */
@GetMapping({ path: [marketSubpaths.historical] })
  public async sendHistoricalData(req: Request, res: Response) {
    try {
      const options = parseQueryToOptions(marketName.historical, req.query);

      if (!isOptionsValidate(options)) return res.sendStatus(404);

      /** SSE Response Instance 생성 */
      new SSE(res, options);

      const data = await this.marketService.getCachedHistorical(options);
      this.writeData(data, res);

      const intervalTime = times.sse * 3000;
      const eventSourceInterval = setInterval(async () => {
         const data = await this.marketService.getHistorical(options);
         this.writeData(data, res);
       }, /** 15s */ intervalTime);

      /** 연결 끊어지는 경우 */
      res.end(() => {
        clearInterval(eventSourceInterval);
        console.log(`SSE Ended`);
      });
    } catch (e) {
      console.error(e);
      res.sendStatus(500);
    }
  }

  private async writeData(data, res) {
    res.write(`id: ${new Date()}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }

브라우저에서는 EventSource라는 웹 API를 사용합니다. open, message, error, close 이벤트 각각에 대한 로직만 addEventListener로 등록해주면 됩니다.

클래스로 서비스 로직을 구현하다 보니, Vue 컴포넌트 내부 데이터를 변경하기 위해 dataCarrierProxy 객체를 사용하는 부분 정도 말고는 생소하지 않은 코드죠

/**
 * @see [https://github.com/zuminternet/investing-app-clone/blob/main/packages/wiii/frontend/services/chart/eventSource.ts](https://github.com/zuminternet/investing-app-clone/blob/main/packages/wiii/frontend/services/chart/eventSource.ts)
 */

export default class EsService {
  url: string;
  private observer: object;
  private es: EventSource;
  private location: string;

  constructor(query: GetHistoricalOptions, dataCarrier: ProxyConstructor) {
    this.url = this.setUrl(query);
    this.observer = dataCarrier;
    this.location = location.href;

    this.es = new EventSource(this.url);

    this.addEventListers();
  }

  /**
   * sourceUrl
   * undefined로 들어온 쿼리만 제거하고 나머지 예외처리는 서버에서 담당
   */
  private setUrl(query: GetHistoricalOptions) {
    ...
  }

  private addEventListers() {
    this.es.addEventListener(`error`, this.onError.bind(this));
    this.es.addEventListener(`open`, this.onOpen.bind(this));
    this.es.addEventListener(`message`, this.onMessage.bind(this));
  }

  /** 서버 연결 시 로직 */
  private onOpen() {
    console.info(`[SSR:Client] Connection Opened`);
  }

  /** 서버 데이터 받을 때 로직 */
  private onMessage({ data }) {
    /** 페이지 이동시 종료 */
    if (this.location !== location.href) this.onClose();
    try {
      this.observer['data'] = Object.freeze(JSON.parse(data));
    } catch (e) {
      console.error(e);
    }
  }

  /** 에러 발생 시 로직 */
  private onError(e) {
    const {
      target: { readyState },
    } = e;
    readyState === 0
      ? console.log(`[SSE:Client] EventSource on Waiting for Server..`)
      : console.error(`[SSE:Client] EventSource on Error`);
  }

  /** 서버와의 연결 끊어질 때 로직 */
  private onClose() {
    this.es.close();
    console.warn(`[SSR:Client] Connection Closed`);
  }
}

43

위 코드를 devServer로 실행시키면 위와 같이 일정 간격으로 계속해서 차트를 그리게 됩니다. 약 200개 캔들과 이동평균선, 거래량을 캔버스로 그리는 게 했는데, API 요청부터 차트 생성 완료까지 짧게는 20ms 정도밖에 걸리지 않더라구요.

SSE를 적용했을 때의 문제점

다해서 200줄가량의 간단한 코드인데요, 사실 여기까지 하는데 약간의 시행착오가 있었습니다. 무엇보다 기술 난이도가 어렵다기보다는 제대로 된 레퍼런스를 구하기 어려웠기 때문이죠..

일례로 백엔드 SSE.tswriteData 메서드 코드를 보시면,

  private async writeData(data, res) {
    res.write(`id: ${new Date()}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }

이렇게 iddata를 명시해서 해당 데이터가 어떤 데이터인지 내용은 뭔지 알려줘야 합니다. 이런 간단한 내용에 대해서도 정확히 명세가 적힌 곳이 없어서 블로그, StackOverflow 등등 여기저기를 검색해보고 이것저것 시도해봐야 하더라구요. 결국 구현하고 나서도 이렇게 하는 게 맞나 하는 확신이 잘 들지 않다보니, 다른 프로젝트에서도 사용할 수 있을지에 대해 회의를 가질 수밖에 없었습니다.

44

또 앞서 SSE는 모던 브라우저에서 사용가능하다고 말씀드렸는데요, IE에선 SSE-EventSource를 지원하지 않아서 polyfill을 사용해야 합니다. polyfill을 사용하더라도, IE 9~10 버전에선 HTTP 동시 연결 가능한 수가 6개여서 SSE가 연결 하나를 계속 잡고 있게 되면 다른 API들, 특히 광고나 로그 관련 API를 사용하지 못하게 될 수도 있죠.

저희 서비스 사용자 상당수가 여전히 IE를 사용 중이다 보니, 현실적으로 이 기술을 계속 사용하기는 조금 무리지 않을까 하는 생각이 들었습니다.

앞으로 다른 프로젝트에서 적용하기는 어려울 것 같아 아쉽긴 하지만, SSE라는 신박한 기술을 적용해보기 위해 JS의 Proxy도 써보고, HTTP에 대한 고민도 해봤던 재밌는 경험이었습니다.

5. 회고

5.1 재민 님의 회고

실무를 앞두고 필요한 기술 스택 및 도메인 압축적 습득

입사 전엔 바닐라 JS, React로 프로젝트를 해왔습니다. Vue.js는 입사가 결정된 이후로 간단한 프로젝트를 통해 맛만 보았습니다. 그리고 TypeScript, 사내 Core.js, CANVAS API 등 새롭게 챙겨야 할 기술들도 많았습니다.

기술을 적용해가며 배우는 것이 최고의 학습법이었습니다. 그저 문서만 읽어가며 공부하는 것보다 프로젝트를 진행하며 짧은 시간 내에 필요한 기술을 압축적으로 습득할 수 있었습니다.

Vue 라이프사이클과 Directive에 대한 깊은 이해를 통해 Vue.js를 능숙하게 사용할 수 있게 되었습니다. 그리고 클라이언트 - 서버 간 잘 정의된 인터페이스들을 공유하여 데이터 흐름을 통제하고 버그를 줄이는 노하우도 얻을 수 있었습니다.

CANVAS API로 차트를 만들고 고도화하면서 마우스, 손가락 상호작용 이벤트 핸들링, 데이터 기반 드로잉을 진행함으로써 이벤트 핸들링에 깊은 이해를 얻을 수 있었습니다. 그리고 주식 등 자산에 깜깜이였던 제가 금융에 관심도 생기고 어느 정도 이해하게 되었습니다.

실무 전 파일럿 프로젝트를 진행하지 않았다면 적응하는 데 오랜 시간이 필요했을 것으로 생각합니다.

나름 피드백 루프가 빨랐던 협업 경험

협업 관련해서 도경 님이 이미 설명해주셨지만, 제가 느낀 바도 적어보겠습니다!

45 매일 10시 데일리 스크럼 내용 정리

데일리 스크럼을 통해 서로의 진척도, 나아가 전체 프로젝트의 진척도를 추적, 가늠해볼 수 있었습니다. ‘프로젝트가 진행되고 있다’ 라는 확신을 갖고 일할 수 있었습니다. 이슈 공유하여 같이 고민해보면서 해결책을 찾는 창조적 논의의 경험도 해볼 수 있었습니다. 혼자 고민하는 것보다 같이 고민하는 것이 낫다는 당연한 진리를 재확인할 수 있었습니다.

46 코드리뷰를 받은 후 MERGE된 PR

하루 동안 작업한 내용을 PR로 올리면 다음 날 코드리뷰를 진행하고 머지하였습니다. 매일 꾸준히 하면서 리뷰안된 PR이 남지 않도록 노력했습니다.

코드를 피드백하면서 서로의 발전을 도모하고 리뷰를 거친 검증된 코드들로 프로젝트가 진행된다는 점이 무척 좋았습니다. 그리고 각자 코드들을 다 보게 되기 때문에 코드의 히스토리도 공유할 수 있었습니다.

무엇보다 피드백루프를 데일리로 짧게 가져감으로써 좀 더 프로젝트를 가볍고 민첩하게 수행할 수 있다는 점이 매력적이었습니다!

5.2 도경 님의 회고

업무에 필요한 기술 셋을 공부하고 다른 사람들과 협업하면서 내가 지금 부족한게 무엇이고 앞으로 어떤 것을 공부해야 하는지 알게 되었던 좋은 시간이었습니다. 부족한 점들이 느껴질 때마다 아직 갈 길이 멀었구나를 일깨워주면서 더욱 빠르게 성장하고 싶은 욕심을 불어넣어 주었던 시간이기도 했습니다.

이제 파일럿 프로젝트를 마치고 실무에 참여하게 되는데요, 좋은 기회를 만들어주신 팀원분들께 감사드리고 앞으로 좋은 모습 보여드릴 수 있도록 하겠습니다.

5.3 정훈 님의 회고

입사 전에는 React로 프로젝트 경험을 했기 때문에 Vue 생태계에 익숙해지는 것이, 이 프로젝트에서 가장 중요한 목표 중 하나였습니다. 프론트엔드만 구현하는 프로젝트였다면 데이터 흐름을 완전히 파악하지 못했을 수도 있는데, (거의) 풀스택으로 진행한 프로젝트였기 때문에 오히려 Vue 라이프사이클과 Vuex 같은 상태관리 라이브러리를 통한 데이터 흐름을 좀 더 잘 이해할 수 있었던 것 같습니다. 어떤 데이터를 가공하고 정제할 때 이걸 브라우저에서 하는 게 좋을지, 서버에서 하는 게 좋을지, 컴포넌트 안에서 하는 게 좋을지 Vuex에서 하는 게 좋을지 고민하는 과정도 재미있었어요.

다만 초반에 TDD를 해보고 싶었는데, 유닛 테스트E2E 테스트 개념도 잘 모르고 무작정 이것저것 시도해보다가 시간을 소비했던 게 제일 아쉬운 것 같습니다. 거의 1주일을 날리다 보니 차트에 기능 추가도 제대로 못 하고 다른 기능들도 급하게 구현하게 됐고, 결국 그 테스트 코드들은 아주 극 초반에 테스트를 시도했던 흔적만 남게 되었거든요. 개인 프로젝트로 좀 더 공부하면서 나중에 실무에도 적용할 수 있으면 좋을 것 같아요.

또 다른 한 가지는 앞서 SSE 적용기에서도 아쉬웠다고 말씀드린 부분인데요, MongoDBTypeORM 적용했던 부분, VuexVuex-Module-Decorators 적용했던 부분들도 레퍼런스가 충분치 않아서 많이 헤맸던 부분들입니다. 물론 제가 검색을 잘 못 했을 수도 있겠지만, RDBMS에서 사용할 수 있는 많은 TypeORM API들이 MongoDB에서는 지원되지 않는 것들이 많다든지, Vuex-Module-Decorators를 제대로 사용하려면 class-validator 같은 추가적인 패키지 설치가 필요하다든지 이런 내용들이 공식 문서에는 명확하게 나타나지 않더라고요. 그래서 좋아 보이는 신기술을 사용해보는 것도 좋지만, 일단 모두가 익숙한 기술을 잘 써보고 나서 조금씩 바꿔보는 것이 좋겠다는 생각을 많이 하게 되었습니다.

6. 마치며

저희 신입 개발자 3인방은 실무에서 고군분투하고 있습니다. 그렇다고 다들 버겁다고 느끼기보단 재미나게 일하고 있습니다.

물론 입사 전부터 쌓아온 실력이 뒷받침되어 행복 개발을 하고 있겠죠. 그러나 앞서 진행한 파일럿 프로젝트가 좋은 자양분이 되었다고 생각합니다. 바로 실무에 투입되 학습과 동시에 일을 처리했다면, 할 수야 있었겠지만, 개발자 개인도 힘들고 회사에 좋은 소프트웨어를 딜리버리할 수 없었을 것입니다.

파일럿 프로젝트를 통해 앞서 필요한 스킬, 도메인을 학습한 것이 소프트하게 온보딩하고, 실무를 위한 발판을 마련하는 데 도움이 되었습니다.

이런 훌륭한 문화, 우수한 팀원을 가진 개발 조직에서 일할 수 있다는 점이 저희에겐 큰 축복입니다. 늘 감사하며 더욱 발전시켜 앞으로 합류할 개발자들과 나눌 수 있도록 노력하겠습니다.

긴 글 읽어주셔서 감사합니다!