티스토리 뷰

1. Custom DOM Library

 DOM API를 통해 요소 노드를 불러오는 것은 꽤나 번거로운 일이다. 이를 해결하기 위해 Lv.1-1 자동차 경주 미션에서 jQuery 스타일로 코드를 작성하는 방법을 익혔다. 그리고 강의 시간에 준이 이보다 더 개선된 형태의 custom DOM library를 만들어봐도 좋을 것 같다는 이야기를 해주었다. 페어였던 카일이 주도하여 custom DOM library를 만들었고 그 코드는 다음과 같다.

export const $ = (() => {
  const constructor = function (selector) {
    if (!selector) {
      return;
    }
    this.targets = document.querySelectorAll(selector);
    this.target = this.targets.length === 1 && this.targets[0];
  };

  constructor.prototype.each = function (callBack) {
    if (!callBack || typeof callBack !== 'function') {
      throw new Error('Custom DOM Library Error: this method needs callback function!');
    }

    this.targets.forEach((target, idx) => callBack(target, idx));

    return this;
  };

  constructor.prototype.map = function (callBack) {
    if (!callBack || typeof callBack !== 'function') {
      throw new Error('Custom DOM Library Error: this method needs callback function!');
    }

    return [...this.targets].map((target, idx) => callBack(target, idx));
  };

  constructor.prototype.filter = function (callBack) {
    if (!callBack || typeof callBack !== 'function') {
      throw new Error('Custom DOM Library Error: this method needs callback function!');
    }

    return [...this.targets].filter((target, idx) => callBack(target, idx));
  };

  constructor.prototype.addClass = function (className) {
    if (className === undefined) {
      throw new Error('Custom DOM Library Error: this method needs classname!');
    }

    this.each(target => target.classList.add(className));

    return this;
  };

  constructor.prototype.removeClass = function (className) {
    if (className === undefined) {
      throw new Error('Custom DOM Library Error: this method needs classname!');
    }

    this.each(target => target.classList.remove(className));

    return this;
  };

  constructor.prototype.toggleClass = function (className) {
    if (className === undefined) {
      throw new Error('Custom DOM Library Error: this method needs classname!');
    }

    this.each(target => target.classList.toggle(className));

    return this;
  };

  constructor.prototype.setEvent = function (type, eventHandler) {
    if (type === undefined || eventHandler === undefined) {
      throw new Error('Custom DOM Library Error: this method needs event type and event handler!');
    }

    this.each(target => target.addEventListener(type, eventHandler));

    return this;
  };

  constructor.prototype.getValue = function () {
    return this.target.value;
  };

  constructor.prototype.setValue = function (value) {
    if (value === undefined) {
      throw new Error('Custom DOM Library Error: this method needs value');
    }

    this.each(target => {
      target.value = value;
    });
  };

  constructor.prototype.enable = function () {
    this.each(target => {
      target.disabled = false;
    });
  };

  constructor.prototype.disable = function () {
    this.each(target => {
      target.disabled = true;
    });
  };

  constructor.prototype.show = function () {
    this.each(target => {
      target.style.display = 'block';
    });
  };

  constructor.prototype.hide = function () {
    this.each(target => {
      target.style.display = 'none';
    });
  };

  constructor.prototype.innerText = function (text) {
    this.each(target => {
      target.innerText = text;
    });
  };

  constructor.prototype.innerHTML = function (html) {
    this.each(target => {
      target.innerHTML = html;
    });
  };

  constructor.prototype.isCheckedInput = function () {
    return this.target.checked;
  };

  const instantiate = selector => {
    return new constructor(selector);
  };

  return instantiate;
})();

 

코드가 상당히 길다. 그만큼 편리한 기능을 많이 제공해준다. 먼저 $ 하나로 노드를 한개부터 여러개까지 불러올 수 있다. each라는 프로토타입 메서드를 생성하여 노드가 여러개인 경우에도 굳이 forEach문을 사용할 필요 없이 메서드 체이닝으로 바로 원하는 기능을 수행할 수 있도록 하였다. 이 코드를 이해하기 위해서는 자바스크립트의 프로토타입, 클로저, 즉시실행 함수, this를 알아야한다. 이 중 즉시 실행 함수가 익숙하지 않아 내용을 정리해봤다.

 

1-1. 즉시 실행 함수 (+ 함수 내부에서 함수 반환)

const hiFunction = ((name1) => {
  console.log("hi " + name1);
  return function helloFunction(name2) {
    console.log("hello " + name2);
  };
})("gonnie"); // hi gonnie

hiFunction("jun"); // hello jun
hiFunction("poco"); // hello poco
helloFunction("kail"); // Uncaught ReferenceError: helloFunction is not defined

 

 위 코드의 실행결과를 확인하면 'hi gonnie', 'hello jun', 'hello poco'가 차례대로 나온다. 그 이유는 hiFunction이 함수 표현식으로 함수가 선언됨과 동시에 실행되기 때문이다. hiFunction을 선언함과 동시에 'gonnie'를 인자로 주면서 실행시키기 때문에 name1의 값은 'gonnie'가 되어 "hi gonnie"를 출력한다. 그러나 이후 hiFunction을 호출하면 출력이 "hi"가 아닌 "hello"로 시작하는 것을 확인할 수 있다. 그 이유는 hiFunction이 최초에 실행될 때, 익명함수를 리턴하기 때문이다. 따라서 hiFunction은 이제 helloFunction으로 대체됐다. 그러나 helloFunction은 hiFunction 스코프 내부에서 선언 됐기 때문에 함수 밖에서 helloFunction을 직접 호출하는 것은 불가능하다. 

 

 위처럼 코드를 작성하면서 얻을 수 있는 이점은 무엇이 있을까?

 

 함수는 재사용을 위해 만드는 것이기 때문에 많이 사용될수록 좋다. 그러나 함수 안에 있는 코드 중 딱 한번 실행되기만 하면 되는 코드가 있다. 예를 들어, 초기화를 하는 코드라던가 프로토타입에 메소드를 추가 혹은 삭제하는 코드는 중복 실행될 필요가 없다. 따라서 함수가 여러번 사용되지만 특정 코드는 딱 한번만 실행되게 하면 되는 경우에 위와 같은 방식으로 코드를 작성하면 불필요한 리소스 낭비를 줄일 수 있다. 아래의 코드를 보며 좀 더 자세히 이해해보자

 

const $ = (() => {
  const constructor = function (selector) {
    this.targets = document.querySelectorAll(selector);
    this.target = this.targets.length === 1 && this.targets[0];

    constructor.prototype.each = function (callBack) {
      if (!callBack || typeof callBack !== "function") {
        return;
      }

      this.targets.forEach((target, idx) => callBack(target, idx));
      return this;
    };

    constructor.prototype.setValue = function (value) {
      this.each((target) => {
        target.value = value;
      });
    };

    const instantiate = (selector) => {
      return new constructor(selector);
    };

    return instantiate;
  };
})();

 

함수 선언과 동시에 실행을 하여 프로토타입에 메소드를 생성하는 코드를 딱 한번만 실행하게 한다. 이후 다른 곳에서 함수가 호출될 때는 instantiate 함수를 통해 새로운 인스턴스를 생성하게 한다. 만약 위와 같이 코드를 짜지 않는다면 함수가 실행될 때마다 불필요하게 프로토타입에 메소드를 생성해야할 것이고 이는 바람직한 방법이 아니다. 따라서 위와 같은 방법을 숙지해놓으면 코드를 좀 더 효율적으로 짤 수 있다.

 

 

그러나 위와 같은 방식으로 코드를 작성하는 경우, 동일한 DOM을 조회할 때마다 매번 새로운 인스턴스를 생성하게 된다. 이에 대해 리뷰어에게 여쭤보니 lodash의 memoize 함수를 참고해보라는 말씀을 해주셨다.

 

1-2. memoize

export function memoize(callback) {
  const cache = new Map();

  return function (arg) {
    if (!cache.get(arg)) {
      cache.set(arg, new callback(arg));
    }

    return cache.get(arg);
  };
}

memoize 함수를 이용하면 캐싱을 통해 중복된 계산을 피하고 메모리를 절약할 수 있다. 앞의 custom DOM library 코드는 동일한 DOM을 조회할 때 마다, 새로운 인스턴스를 생성하는 문제가 있다고 언급했다. memoize 함수를 적용하면 이러한 문제를 해결할 수 있다. 그러나, custom DOM library는 memoize 함수를 도입하면 안된다. 그 이유는 이전에 호출했던 DOM이 non-live 객체인 NodeList인 경우, DOM의 상태 변화를 반영하지 않기 때문이다. 따라서 DOM의 상태가 변경되었다면 새롭게 $(selector)로 조회를 해줘야 하는데 memoize를 사용하면 이미 selector가 캐쉬에 저장되어 있어 새롭게 인스턴스를 생성하는 것이 아닌 기존 요소를 리턴한다. 따라서, 사용자는 memoize가 적용된 custom DOM library를 사용하는 한 갱신된 값을 얻을 수 없다. 

 

위와 같은 이유로 memoize를 공부하고 적용했지만 마지막에는 원래의 코드로 돌아왔다. 그리고 본래의 코드에서 우려되었던 새 인스턴스를 매번 생성하여 불필요한 메모리를 차지한다는 문제는 자바스크립트의 가비지 컬렉터가 해결해줌을 깨달았다. 자바스크립트의 가비지 콜렉터는 참조되지 않는 메모리 주소에 대하여 메모리를 해제한다. 따라서 새로운 인스턴스를 생성하고 변수로 지정한 다음에 동일한 이름의 인스턴스를 새로 생성하여 변수에 다시 저장하면 이전 인스턴스는 더 이상 참조되지 않아 가비지 컬렉터에 의해 메모리 상에서 사라진다. 따라서 우려했던 문제는 해결되는 것이다.

 

1-3. 커스텀 라이브러리 사용 시 주의사항

크리스의 리뷰 내용

 

문서화를 잘 해야한다.
다른 팀원이 인터페이스를 익히는데 시간이 필요함을 인지한다.
지속적인 유지보수가 필요하다.
사용자가 라이브러리 제작자의 의도대로 라이브러리를 사용하지 않는 경우 undefined가 아닌 throw new Error를 통해 명시적으로 에러를 알려준다.

 

2. Object.freeze

Object.freeze() 메서드는 객체를 동결합니다. 동결된 객체는 더 이상 변경될 수 없습니다. 즉, 동결된 객체는 새로운 속성을 추가하거나 존재하는 속성을 제거하는 것을 방지하며 존재하는 속성의 불변성, 설정 가능성(configurability), 작성 가능성이 변경되는 것을 방지하고, 존재하는 속성의 값이 변경되는 것도 방지합니다. 또한, 동결 객체는 그 프로토타입이 변경되는것도 방지합니다.  freeze()는 전달된 동일한 객체를 반환합니다. (MDN)
const constants = {
  NAME : "GONNIE"
};

constants.NAME = "KAIL";
console.log(constants.NAME); // 'KAIL'

 

 상수를 편하게 사용하기 위해 const로 객체를 선언해도 객체는 값의 변경이 가능하다. 자바스크립트에서는 원시 값을 변수에 할당하면 변수(확보된 메모리 공간)에는 실제 값이 저장된다. 이에 비해 객체를 변수에 할당하면 변수(확보된 메모리 공간)에는 참조 값이 저장된다. 이 때문에 constants 객체는 const로 선언되었음에도 변경이 가능하다. 하지만 상수는 외부에서 값이 변경되서는 안된다. 이 문제를 해결하기 위해 자바스크립트에서는 Object.freeze 메서드를 제공한다.

 

const constants = Object.freeze({
  NAME : "GONNIE"
});

constants.NAME = "KAIL"
console.log(constants.NAME) // 'GONNIE'

 

 위의 코드와는 다르게 아래의 코드는 객체를 동결시켜주어 객체 외부에서 값을 변경하려는 시도가 있어도 이를 무시하고 원래의 값을 보여준다. 따라서 상수를 한 곳에 모아 객체로 사용하기 위해서는 Object.freeze를 사용하여 외부에서 변경하지 못하도록 하는 것이 좋다.

 

3. 스타일 클래스와 DOM 조작을 위한 클래스 이름 분리

 이번 미션에서 공통적으로 나왔던 리뷰는 DOM 조작을 위한 메서드를 사용하기 위해 어떤 속성을 사용할 것인가였다.

보통 하나의 페이지에서 유일한 요소인 경우에는 id를, 요소가 여러 개일 경우에는 클래스 속성을 사용한다. 

 

타 크루의 리뷰 내용

 

물론, data 속성을 이용해 DOM 노드를 가져올 수 있다. 그러나, 개인적으로 data 속성은 DOM을 조회하기 위한 속성이 아니라 이름 그대로 data에 대한 내용을 저장하기 위한 목적이라고 생각한다. 이에 대해 파노가 아주 좋은 링크를 소개해주었다. (파노 갓...) Airbnb 스타일 가이드에서는 자바스크립트를 위한 클래스명에 'js-'라는 prefix를 붙이는 것을 권장한다고 한다.

 

 

 

 

codemakebros/css-style-guide

CSS와 Sass에 대한 가장 합리적인 접근 방법. Contribute to codemakebros/css-style-guide development by creating an account on GitHub.

github.com

4. Re export

 프로젝트의 규모가 커지면 필연적으로 구조를 분리하여 모듈화를 해야하고 파일의 개수가 점점 늘어난다. 때문에 다른 파일을 export 해오기 위해서는 파일의 경로를 정확하게 알아야한다. 그러나, Re export를 쓰면 이러한 불편을 어느정도 해소할 수 있다. 실제 이번 프로젝트의 util 폴더 구조를 들여다 보자.

 

 

 util 폴더에는 총 4개의 파일이 있다. 만약 외부에서 util과 같은 depth에 있는 폴더의 파일에서 DOM.js 파일을 import 하고 싶으면 상대주소는 "../util/DOM.js"가 된다. 그러나, 다음과 같이 util 폴더 내에 index.js 파일을 추가하고 util 내에 있는 파일을 index.js에서 re export하면 상대 주소가 "../util/index.js"가 된다. (리액트에서는 "../util"까지만 써도 가능)

 

 "../util/DOM.js"과 "../util/index.js"는 큰 차이가 없는데 굳이 이걸 왜 쓰나 싶을 수 있다. 그러나, 후자의 경우에는 특정 함수가 util 내의 어떤 파일에 있는지 고민할 필요가 없다. 즉, util 내의 모든 export 되는 함수들을 단순히 "../util/index.js"으로 import 할 수가 있다.

 

 신기한 점은 index.js에서 import를 하고 export를 하는 것이 아니라 바로 export를 해도 정상적으로 동작한다는 것이다. 이것이 바로 Re export이다.

 

 

util 폴더 내의 index.js
다른 파일에서 util 내의 함수를 import

 

5. UI/UX

 이번 미션의 3단계는 UI/UX가 자유로웠다. 따라서, 동일한 기능을 크루원들마다 각양각색으로 구현하였다. 본인은 3단계 역시 기능 구현에 좀 더 초점을 맞췄는데 하루 같은 경우는 실제 사용자를 위한 고려한 듯한 UI와 UX를 보여주었다.

프론트 개발자에게 사용자는 가장 중요한 존재이다. 앞으로의 미션에서는 단순히 기능 구현에서 그치지 않고 UI와 UX 모두 사용자 친화적으로 설계해야겠다.

 

 

6. 독립적인 테스트의 필요성

 step2까지는 테스트를 설계할 때, 이전 테스트가 다음 테스트와 이어지도록 하였다. 이 방법은 특정 상태까지 가기 위한 반복적인 코드를 줄이고 테스트가 수행되는 작업 시간을 단축할 수 있기 때문에 사용하였다. 그러나 step3에서 새로운 기능이 추가되야하고 이를 위한 테스트를 설계해야하는 과정에서 문제가 발생했다. step2에서 만들어놓은 연속적인 테스트 사이에 새로운 테스트가 추가되는 경우, 앞 뒤로 테스트 코드를 모두 수정해야했기 때문이다. 이를 수정하는 데서 발생하는 비용이 꽤나 컸다. 만약 테스트가 더 복잡했다면 수정에 더 많은 시간이 필요했을 것 같다.

 

 따라서, 이러한 문제를 예방하기 위해서는 코드가 조금 중복되고 테스트에 필요한 수행시간이 늘어나더라도 독립적인 테스트가 필요함을 깨달았다.

 

 

7. 깃 리베이스, 체리픽

 1차 미션 PR을 보내고, merge가 되지 않은 상태에서 2차 미션을 수행하다가 conflict가 발생했다. 1차 미션 PR 커밋로그와 2차 미션 PR 커밋로그가 달라 발생한 일이었다. 2차 미션 PR 커밋로그가 1차 미션 PR이 merge된 내역이 없었기 때문에 이후에 커밋된 모든 내역이 충돌했다. 이 문제는 다른 크루원들도 겪었던 문제였고 갓 하루 선생님의 도움으로 문제를 해결할 수 있었다.

 

 먼저, 리베이스를 통해 upstream의 브랜치와 현재 브랜치를 동기화 시킨다. 이후, 체리픽을 통해 2차 미션 커밋을 가져온다. 

 

 깃은 참 알다가도 모르겠는 친구이다. 이번 문제를 겪으면서 깃을 확실하게 알아야 협업 능력이 올라갈 것 같다는 생각을 가지게 되었다. 그래서 바로 "Git 교과서"라는 서적을 구입했다. 지금은 미션에 치여 읽지 못하고 있지만 방학기간에 읽어둬야겠다.

 

 

Git - 자주 사용하는 명령어 모음

우아한테크코스 과정을 진행하면서 자주 사용하는 git 명령어를 정리해보자. 과거의 커밋 메세지 변경하기 PR 메세지를 작성하기 전 git log를 보며 조금 더 커밋메세지를 자세하게 변경하고 싶을

365kim.tistory.com