개인 블로그 이전하였습니다! https://mobilog.me 아무데나 클릭하면 닫힙니다.
[우아한테크캠프] 2차 프로젝트 회고

2차 프로젝트가 끝났다. 원래는 매주 주말에 작성하려고 했으나......ㅠ

2차 프로젝트의 주요 키워드는 무엇이 있었는지

키워드 위주로 회고를 먼저하고 마지막으로 팀프로젝트 협업 관련 내용을 작성하려고 한다

 

키워드 :

셋팅 관련 : 웹팩, 바벨, SPA 컴포넌트, TypeScript
기술 관련 : prototype, 웹소켓, JWT, Router (History Api), JS처리방식 (event loop), 이벤트 위임
서버 및 백엔드 : AWS EC2 (ubuntu), Mysql

 

1. 시작

2차 프로젝트의 시작은 [컨벤션 짜기, Webpack, 바벨, eslint, prettier] 4가지의 설정으로 시작했다. 사실상 설정도 완전히 끝낸것도 아니고 중간중간 변경하고 추가했었던 기억이 남는다.

간단하게 왜 추가했고 어떤 기능을 가지고 있는지 설명하자면 다음과 같다.

  주요 기능 이유
컨벤션 짜기 커밋 방법 및 여러 협력 방안 내용작성 내용은 많지는 않지만 팀과 협력할때 코드리뷰나 어떠한 것을 수정하였는지 최소한의 알기 쉽게 하기 위함이다.
Webpack Module Bundler 라이브러리들을 각각 추가해주는 불편함 제거 및 의존되는 모듈들을 쉽게 추가가 가능하고 배포할 때 좀더 적은 파일을 올리고 코드를 난독화(Uglyfy)를 할 수 있기 위함이다.
Babel Transfiler 작성한 코드를 설정한 브라우저 Target에 맞추어 변환주기 때문에 코드가 좀더 일정해질 수가 있고 상위 버전의 es 문법을 사용할 수 있다.
EsLint
(TsLint)
코드 문법 검사 협엽간 좀더 나은 코드의 통일성을 유지 할수 있으며 여러 문제들을 작성하면서 실시간으로 알 수 있는 장점이 있다.
Prettier code formatter 이 또한 코드의 통일성의 이유이다. 문제보다는 코드 디자인에 초점에 맞추어져 있어 커밋간 코드 변화를 좀더 쉽게 파악하기 위함이다.

또한 AWS EC2 하나의 서버에만 백엔드 서버, 프론트 서버, DB를 모두 올리다 보니 실 개발보다는 일반 프로젝트의 시작부터 배포까지 간단하게 경험할 수 있는 것이 컸던것 같다.

 

2. 프로젝트 중 겪었던 문제

1. Webpack, Babel 설정

참 위의 설정들은 맨날 react crate app 이런 걸로만 자동 생성된걸 사용했었지 따로 작성해본 경험은 없어 삽질을 좀 한것 같다... 

내용들은 별도의 글로 작성하는 것이 나을 것 같아 짧게 넘기기로했다.

 

2. 초기 코드 셋팅 및 작업 형태 준비 

사실상 이 부분이 가장 어려웠던 문제였다. 어떤 방식으로 코드를 짜야하고 폴더구조는 어떻게 만들어야하며, 파일명이나 폴더명들은 어떻게 구분할 것이며 페이지 수나 구분하는 것들이 가장 어려웠다.

처음 시작할 때에는 SPA로 작업을 하자고 팀원과 이야기가 되었고 이에 맞춰 나아가기로 했다. 이 이후 사실상 어떻게 진행해야될지 감이 안잡혀서 그냥 무작정 메인페이지를 무작정 작성해보며 기본코드를 짜려고 했다.

그 당시 코드를 일부 보게 된다면..

export default function (this: any) {
  const root = $('#root'); // Element 가져오기
  const _Category = Category(); // 카테고리 Element 생성
  const _Location = createElem('div', { //위치 Element 생성
    class: 'location',
  });
  console.log(_Category);
  const _userInfo = createElem('div', { //유저 정보 Element 생성
    class: 'user-info',
  });
  const _Menu = createElem('div', { //메뉴 Element 생성
    class: 'menu',
  });
  const _Header = createElem( //헤더 Element 생성
    'Header',
    {
      class: 'main-header',
    },
    _Category, // 그 동안 생성한 Elements 들을 헤더 elements에 appendchild
    _Location,
    _userInfo,
    _Menu
  );
  this.render = function () { //루트 Element에 appendchild
    console.log('render');
    return root?.appendChild(_Header);
  };
}

참 코드가 더러워지고 재사용성이라고는 1도 찾아볼수 없으며 가장 세부의 element부터 작성하기 때문에 무엇을 위한 코드인지는 함수의 마지막 쯤에 가서 알수 있게 된다. 처음에는 별 생각 없이 일단 짜기나 해보자 라는 생각으로 코드를 작성했었고 실제로 동작하다보니 이러한 형태로 코드를 짜야겟다고 팀원에게 얘기했었다. 이부분에 대해서는 비효율적이고 안좋다는 것을 알면서도 더 찾아보려고 노력하지 않았다는 점을 반성하기도 했다..;;;

사실 얼마안가 바로 위와같은 형태의 코드를 바로 포기하고 바로 다음 방안을 찾기도 했다.

그 다음으로 찾은 것이 컴포넌트 형태이다. 

export default class Component {
  constructor($target: HTMLElement, $props?) { //new 객체 생성시 자동 실행 변화 x
    this.$target = $target;
    this.$props = { ...$props };
    this.setup();
    this.render();
    this.setEvent();
  }
  setup() {} //컴포넌트 실행시 적용할 설정 및 변수들 작성
  mounted() {} //innerHtml 이후 실행될 돔조작 등을 위한 함수
  template() { //html형태의 템플릿만 작성
    return ``;
  }
  render() { // innerhtml 실행
    this.$target.innerHTML = this.template();
    this.mounted();
  }
  setEvent() {} //각종 이벤트 등록을 하기위한 함수
  setState(newState) { // 현재 컴포넌트의 상태를 변경하기 위한 함수 (변경시 리렌더)
    this.$state = { ...this.$state, ...newState };
    this.render();
  }
  addEvent(eventType, selector, callback) { //부모 노드의 하위 노드중 selector에 해당하는 노드에 이벤트를 등록
    const children = [...this.$target.querySelectorAll(selector)];
    const isTarget = (target) => children.includes(target) || target.closest(selector);
    this.$target.addEventListener(eventType, (event: Event) => {
      if (!isTarget(event.target)) return false;
      callback(event);
    });
  }
}

모든 페이지 또는 컴포넌트에 해당되는 파일은 위와 같은 형태로 작성되었으며 좀더 가독성이 있고 통일된 형태의 코드가 진행될수 있었다.

처음에는 함수형으로 작성하려하여 함수형으로 변형했었지만 함수형의 경우 prototype으로 상속을 받으면서 사용해야 함수들이 중복적으로 생성되지 않으며 메모리 절감에 효과가 있다는 것을 조언을 통해 알게 되었고 이는 별도로 설정을 해주어야 했다.[1]

별도로 설정을 매 함수마다 해주었으면 되었었지만 그냥 이럴바에 클래스를 쓰자는 의견으로 변경되어 클래스로 변경한 기억이 남는다 ㅎㅎ;;

 

이렇게 기본적인 베이스 세팅을 가지고 프로젝트를 진행하였다.

이번 문제 이후 프로젝트를 위한 기본적인 보일러 플레이트 코드를 만들어 놓는 것도 좋을 것 같다.
이번번 프로젝트에 진행한 웹팩, 바벨, eslint, prettier, component 등의 세팅 코드는 별도로 git에 레포를 만들어 놓아야지.

 

3. Event Loop에 대한 모자란 지식... [3][4]

캐러셀 슬라이드를 만들고 있었는데 innerHTML이후 img태그의 offsetwidth의 값을 계속해서 0으로 가져오는 현상이 발생했었다. 렌더링 된 이후 다시 해보면 정상적으로 width값을 가져오는데 이에 대한 이해를 당시 명확하게 하지않고 setTimeout으로 비동기로 값을 찾은것으로 해결했다. 이 부분을 되돌아 보자면 사실상 코드 내에서는 비동기로 작동하는 코드는 하나도 없었다.

단지 innerHTML을 하고 화면에 렌더링을 안해줘서 img 사이즈를 가져오지 못하는 것이 원인이 었는데 innerHTML이후 왜 렌더링 또한 바로 안하는지를 찾아보게 되었고

이는 별도로 렌더링을 위한 큐 비스무리한게 따로 있는 것을 알게 되었다.

(아직도 조금 헷갈리는데 이게 렌더링 큐가 있는건지. 아니면 주기적으로 테스크 큐에 렌더링 UI 콜백을 넣어주는건지 모르겠다.)

다음에 간단하게 테스트를 하기 위한 코드가 있다.[2]

console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(function() {
  console.log("promise1");
}).then(function() {
  console.log("promise2");
});

requestAnimationFrame(function() {
    console.log("requestAnimationFrame");
})
console.log("script end");

실행 순서는 어떻게 될까?

크롬 콘솔창을 열어 결과만 보자면와 같이 출력된다.

script start
script end
promise1
promise2
undefined
requestAnimationFrame
setTimeout

위 내용을 토대로 보자면 Micro Tesk인 Promise 다음에 AnimationFrame이 뒤에 있음에도 불구하고 SetTimeout 보다 먼저 실행된다.

requestAnimationFrame 함수를 가장위로 올려봐도 동일한 결과가 나온다.

 

아주 가끔 requestAnimationFrame 과 setTimeout의 실행 순서가 다르게 나타나기도 한다. 

그렇다면 requestAnimation을 담당하는 render Quere가 따로 있는 것이 아닌가 라는 생각에 찾아보았지만 어떤곳은 UI render도 Macro Queue에 넣진다고 하고 어떤곳은 render Quere가 따로 있다고도 한다.. 많이 헷갈리기 때문에 별도 글로 다루려고 한다.

 

내용이 조금 다른길로 빠졌지만 render를 해주는 것 자체도 일종의 비동기다보니 콜스택에 함수가 쌓여있어 동작이 되지 않는 것이 원인이었다. 따라서 setTimeout을 통해 콜스택이 비어있는 상태로 만들고 렌더링을 해준 후 offsetwidth를 찾아내는것으로 해결이 되었던 것이다.

 

3. 웹소켓으로 채팅 연결하기

이 또한 내용이 길어 별도로 글을 작성하는 것이 나을 듯하다.. 처음에는 socket.io를 이용하여 만들려고 했으나 이런 저런 오류로 인해 websocket으로 전환하여 작업을 진행했다. 대부분의 문서들이나 질문 글이 다 require 모듈로 되어있어 찾느라 조금 고생한 기억이 있다.. 웹소켓은 보통 url의 경로를 이용하여 주로 작업을 진행하는데 이번 팀의 경우 router를 #를 이용하여 변경했기 때문에 url origin을 제대로 가져오지 못하는 점이 문제가 있었다. 그래서 채팅방 url에 접근했을 경우 유저 체크와 캐시에 정보를 저장하여 보내는 방식으로 해결을 하였는데 이때의 해결방식과 채팅방을 여러개로 작성하여 구분하는 것과 연결법 등 내용을 자세하게 작성하여 추후에도 내가 해메지 않도록 작성해야겠다...

 

3. 프로젝트 중 적용한 내용

1. 이벤트 위임

이번의 경우 주로 컴포넌트를 이용했기 때문에 이벤트 위임형태를 많이 사용하였다. 상위 노드에 e.target 형태로 주어서 접근하였고
하위노드에서는 e.closest를 이용해서 상위 노드로 접근하여 클래스나 각종 변경을 이하였다.
이 점은 이벤트 리스너를 보다 적게 달아도 되는 점에서 효율성이 굉장히 높게 나타난다는 점에서 이득이 있다. 단점은 target을 이용하다보니 적용할 노드의 안쪽 노드가 target이 될경우 적용이 되지 않는 점이 문제가 있었다. 이는 위쪽의 closest를 이용하여 해결을 했지만 closest는 찾지 못하면 root 노드까지 찾아가기 때문에 노드의 깊이가 깊어진다면 비효율적인 측면이 없지않아 있는 것 같다.

 

2. JWT

프로젝트 로그인 및 인증관련해서는 JWT를 채택하여 사용하였다. 일반적인 세션보다 이점이 있고 어떤것이 장점이기 때문에 사용했다기 보다는 한번 적용을 해보고 싶었고 이를 배우고 알아가고 싶었기 때문에 적용했던 점이 더 컸었다. 

문제는 리프레시 토큰을 구현하는 점이었는데 access_token이 만료가 되면 만료 메세지를 보내고 refresh_token과 함께 다시 refresh api로 fetch 하여 검사후 다시 access_token을 받는 형태로 진행해야 하는데

이 과정을 "프론트에서 어떻게 다시 보내주지??" 라는 생각과 함께 refresh api를 다 구현하고 적용은 하지 못했던 점이 있다. 지금 와서 생각해보니 아니면 response메세지로 전달하고 이것을 가지고 다시 fetch를 했으면 되었는데 이부분은 생각이 많이 짧았던 탓에 모자랐던 것 같다..

 

3. Hitory API (router)

이 부분은 다른 팀의 도움을 많이 받았다. 사실상 hash(#)으로 거의 구현은 되었었으나.... 실제 오류나 정상적으로 동작하지 않는 부분이 많아 도움을 받았었다. 이 또한 별개의 주제로 내용을 다뤄볼 예정이다.

 

4. 협력에서의 아쉬운점.

 고민을 많이 했었던 프로젝트였다. 사실상 협업에 관해서는 굉장히 매끄럽게 되고 진행사항에 큰 문제는 없었지만 아쉬운점을 뽑는다면 다음과 같다.

 

1. 큰기능을 구현하는 경우 (Ex: 채팅, 글 작성 등)

이 문제는 사실상 협력보단 업무 분담이 잘될 경우의 케이스라고도 할 수있다. 실제 구현자체에서는 잘마무리가 되었고 문제도 없었지만 구현하는 과정이 문제였다. 바로 다른사람의 코드를 이해하는데 시간이 걸리는 것이었다.

사실상 페어로 진행하기에는 시간이 오래걸리는 작업이고

그렇다고 코드리뷰를 통해 이해하려고 하면 내 코드가 아니기 때문에 이해도가 낮고 이미 정상적으로 구현되어서 이해하려 하는 의지도 조금 줄어들게 된다. 다음 작업이 많이 남아있기 때문에..!!

위 두가지가 가장 큰 난제였고 결국 마땅히 해결책은 없었지만 종료 회고를 통해 다음 프로젝트에서는 그래도 주석처리 진행을 해보자는 의견을 통해 나름 최소한의 해결책을 마련했었다.

 

2. 깃의 컨벤션 등의 문제

이는 문제라기보다는 필요성에 의한 문제이다. 사실상 커밋, PR, ISSUE등의 큰 작업을 할 때를 위한 간단한 컨벤션을 작성하였고 이를 따라 진행에도 크게 벗어나지 않게 하여 작업을 진행했었다. 다른 팀의 경우 엄청 세부적이게 작성한 팀도 있고 커밋내용도 많고 했지만 우리 팀의 경우 꽤나 간결했다. 

이러한 이유는 대부분의 시간이 협업이었고 화상채팅을 이용하여 실시간으로 코드를 보고 리뷰하고 문제를 같이 의논하는 경우가 많아 깃에 쓸 내용이 별로 없었다. 물론 그 협업에 대한 내용들을 글로 남겨 놓는 것 또한 추후에 도움이 되기 때문에 글을 작성하는 것은 옳다고도 할수 있다.

사실상 우리 팀만 보고 우리팀에서만 진행한다면 큰문제는 없는 방식이라고 생각된다.

하지만 다른팀도 보고 어떤내용인지 이해하기 위해서는 충분한 설명과 충분한 정보들이 작성되야 한다고 프로젝트 마지막 쯤에 멘토링을 하면서 깨닫게 되었다. 여전히 단순 우리팀 또는 나만의 코드라면 깃 컨벤션은 크게 중요하지 않다고 생각된다. 하지만 타인을 생각하는 경우에는 이야기가 달라진다는 점을 알고 다음프로젝트는 좀더 엄청까지는 아니어도 좀더 구분을 짓고 알아보기 쉽게 작성 컨벤션을 짜보려고 한다.

 

 

5. 최종회고

이번은 구현하는 기능이 저번보다 많아서인지 배우고 협력하는 과정이 너무 마음에 들었던 프로젝트였다. 어떤 프로젝트건 진행하고 나면 아쉬운점이 여럿 남는 것이 안타깝기는 했지만 이 것들을 기반으로 다음에 더 잘할 수 있다는 자신감이 생기기도 한다.

사실 이번 팀원이 소통을 잘해주고 서로 모르는 것을 너무 잘 물어봐주고 이야기 해줘서 더 원만하고 잘 해결되었던 것도 굉장히 컸다.

여럿 처음해봐 어려운 기술들이 몇가지 있었지만 원만하게 잘 해결할 수 있던 것도 서로간의 의견제시가 가장 컸었던 것 같다.

 

이번 팀플에서는 부담없이 의견을 제시하고 그러한 의견들을 듣고 충분히 리뷰를 한다는 점이 굉장히 크게 다가온 경험이었다.

 

별도로 글을 작성할 내용들은 다음과 같다

1. Webpack, Babel

2. Event Loop

3. Websocket

 

이 외에도 추가로 생각나는 것들이 있으면 작성할 예정이다. 원래는 사실 채팅 구현도 못끝낼줄 알았지만 어찌어찌 되어서 기분은 좋았다.

다른 것들을 이야기 하자면 다른 팀들이 너무 잘만들었다는 점. 2주나 더 지났지만 여전히 다른 참가자 분들께 배울 것들이 많다는 점이다.

 

참고내용

[1] ProtoType 이해

[2] Event Loop

[3] window.requestAnimationFrame

[4] 어쨌든 이벤트 루프는 무엇입니까? | Philip Roberts | JSConf EU