Immersive Course

Immersive Course(이하 IM)는 프로그래밍 교육기관 코드 스테이츠의 웹 개발 심화 코스이다. 나는 IM 16기로 2019년 11월부터 2020년 2월까지 다른 수강생들과 함께 공부한다. 해당 기간에 TWIL에 정리하는 내용은 IM에서 배우고, 내가 찾아보고, 다른 수강생들이 전해 준 지식이다.


1. 페어 프로그래밍에서 GitHub와 Git 명령어 사용하기

IM에서 주어지는 과제들은 대부분 페어 프로그래밍으로 두 사람 혹은 세 사람이 함께 풀어나간다. 과제를 푸는 방식을 제안하는 네비게이터, 네비게이터의 이야기를 듣고 코드를 작성하는 드라이버로 역할을 나누어 의견을 교환하며 문제를 해결한다.

페어 프로그래밍을 진행하면서 사용하는 Git 명령어와 GitHub 이용법을 간단하게 정리했다.

  1. 필요한 repository를 GitHub에서 fork하여 내 공간에 가져온다.
  2. 해당 repository를 내 local로 clone하여 가져온다.
➜ git clone https://github.com/내 GitHub아이디/repository 이름
  1. 페어의 repository에 접근해 파일을 가져올 수 있도록 페어의 repository를 내 repository의 remote 중 하나로 설정한다.
➜ git remote add pair https://github.com/페어의 GitHub아이디/해당 repository 이름

// 여기서 pair는 고유 명령어가 아니라, 리모트를 식별하기 위해 붙이는 이름이다. 
// 페어의 이름이나 별명 등 원하는 이름을 설정할 수 있다.
  1. 코딩 시작. 내가 드라이버라면, 네비게이터와 함께 코드를 작성하고 코드를 저장한 후 내 repository(remote origin)에 push한다.
➜ git add . 
// 수정한 모든 파일을 stage에 올린다.

➜ git commit -m '커밋 메시지' 
// 커밋 메시지와 함께 커밋한다.

➜ git push origin 브랜치명 
// branch에서 작업중인 경우 해당 branch의 이름을, master에서 작업중인 경우 master를 입력한다.
  1. 이제 역할을 바꿀 차례. 내가 네비게이터, 페어가 드라이버를 맡는다. 드라이버는 3번에서 remote로 등록해 둔 상대방의 repository에서 자신의 local로 코드를 가져온다.
➜ git pull pair master

// pair라는 이름을 붙인 리모트에서 코드를 받아 오는 경우이다. 등록한 이름이 hello라면?
// git pull hello master 라고 입력하면 된다. 
// master가 아니라 branch를 만들어 작업중이라면, master 자리에 해당 branch명을 입력한다.
  1. 과제를 모두 마무리할 때까지 4번과 5번을 반복한다.

2. Linter와 Tester

1. Linter

Linter는 코드의 스타일과 룰을 설정하고, 내가 작성한 코드가 그 설정을 잘 지키고 있는지 알려주는 툴들을 총칭한다. 유지 및 보수가 쉽고 읽기 편한 코드를 작성하는 데 도움이 된다. 두 사람 이상이 함께 코드를 작성하는 프로젝트에서라면 필수적이다. 여러 가지 자바스크립트용 Linter 중 IM에서 주로 사용할 Linter는 ESLint다. 그리고 부수적으로 Prettier를 사용해도 좋다.

2. Tester

지금까지는 주로 console.log()debugger, 개발자 도구를 이용해 작성한 코드가 의도대로 작동하는지 알아보았다. 하지만 두 사람 이상이 함께 작성하거나, 양이 많은 코드라면 그 방법으로는 한계가 있다. 내가 원하는 목적을 내가 쓴 코드가 제대로 수행하고 있는지 체크하려면 Tester를 사용하면 좋다. IM에서는 주로 JEST를 사용한다.

JEST : toEqual() / toBe()

JEST에는 조건과 객체들을 비교하는 여러 가지 메서드 (Matchers)가 있다. 그 중 expect와 함께 사용하는 toEqual()toBe() 는, 어떤 객체가 내가 미리 테스터에 입력한 객체와 같은지를 판단하는 메서드다. 둘 다 Object.is() 메서드를 내부적으로 사용한다.

[ toEqual() | JEST ]
toEqual()은, 비교할 두 참조형 객체의 요소 하나하나를 꺼내서 비교한다. 예를 들어 arr1 = [1, 2]arr2 = [1, 2] 를 비교할 때, 각 배열 속 요소 12 를 꺼내 그 요소들이 동일한 요소인지를 비교한다.

arr1arr2 는 각자가 저장된 주소는 다르나, 요소 12 는 같은 주소에 저장된 값이기 때문에 toEqual() 은 두 배열을 같다고 판단한다. 눈으로 보이는 겉모양이 같은 객체라면 둘은 같다고 판정된다 할 수 있다.

const arr1 = [1, 2];
const arr2 = [1, 2];

test('toEqual TEST', () => {
  expect(arr1).toEqual(arr2);
}); 
// true

[ toBe() | JEST ]
toBe() 는 대부분 원시형 객체를 비교하는 데 사용한다. 참조형 객체의 경우 요소 하나하나를 꺼내 비교하는 것이 아닌, 그 객체 자체의 주소 값을 비교한다. 그러므로 겉모양이 같더라도, 주소 값이 다른 객체라면 둘은 다르다고 평가된다.

const arr1 = [1, 2];
const arr2 = [1, 2];

test('toBe TEST', () => {
  expect(arr1).not.toBe(arr2);
});
// true 
// toBe() 메서드 앞에 not을 붙이면 '이 둘은 서로 다른가?'라는 물음이 된다.

Object.is()

[ Object.is() | MDN ]
Object.is() 메서드는 두 값이 같은 값인지 결정한다.


Object.is()는 두 값이 같은 값인지 결정한다. 다음 중 하나를 만족하면 두 값은 같다.

  • 둘 다 undefined
  • 둘 다 null
  • 둘 다 true 또는 둘 다 false
  • 둘 다 같은 문자에 같은 길이인 문자열
  • 둘 다 같은 객체
  • 둘 다 숫자이며
    • 둘 다 +0
    • 둘 다 -0
    • 둘 다 NaN
    • 둘 다 0이나 NaN이 아니고 같은 값을 지님

이는 == 연산자에 따른 같음과 같지 않다. == 연산자는 같음을 테스트하기 전에 양 쪽(이 같은 형이 아니라면)에 다양한 강제(coercion)를 적용하지만("" == falsetrue가 되는 그런 행동을 초래), Object.is는 어느 값도 강제하지 않는다.
또한 이는 === 연산자에 따른 같음과도 같지 않다. === 연산자(와 == 연산자 역시)는 숫자값 -0+0을 같게 Number.NaNNaN과 같지 않게 여긴다.

3. ES5에서 Object를 생성하는 함수 구현하기 : Instantiation Patterns

[ Instantiation Patterns in JavaScript | Jennifer Bland ]
[ JavaScript 객체 소개 | MDN ]
[ The Definitive Guide to Object-Oriented JavaScript | James Shore ]

공통점이 많으면서 세부적인 사항만 다른 여러 Object를 쉽게 만들고 싶을 때, 그 Object들의 공통점을 모아놓은 함수를 만들어 마치 붕어빵 틀처럼 사용할 수 있다. 그 틀에서 구워진 붕어빵 Object들은 인스턴스 instance라고 부른다.
붕어빵 틀은 class라는 개념을 가진 다른 언어에서는 class라고 부를 수 있겠으나, 자바스크립트에는 class라는 개념이 없기에 생성자 함수 혹은 constructor라 부른다.

ES6에는 생성자 함수를 비교적 간단하게 구현할 수 있는 문법 class가 있다. class문법이 없는 ES5에서 함수를 이용해 instance를 생성하는 방법은 네 가지다.
네 가지 스타일을 구현한 예시 코드는 자료구조 중 Stack을 간단하게 구현한 코드다. Stack은 하나의 대문으로 자료가 들어가고 나오도록 짜여 있어서, 가장 처음 들어간 데이터는 자기 위에 쌓인 데이터가 나갈 때까지 나갈 수 없다. 가장 최근에 들어온 데이터가 가장 처음에 나온다.

1. Functional

우선 틀 역할을 할 함수를 만든다. 이 함수를 앞으로 메이커 함수라고 부르겠다. 메이커 함수는 내부에 객체를 하나 가지고 있고, 앞으로 만들 인스턴스들의 공통적인 속성과 메서드를 그 객체 안에 모두 담고 있다. 마지막에 해당 객체를 리턴한다.

const Stack = function () {
  const someInstance = {};

  const storage = {};
  let count = 0;

  someInstance.push = function (value) {
    storage[count] = value;
    count++;
  };

  someInstance.pop = function () {
    if (count === 0) return;
    count--;
    return storage[count];
  };

  someInstance.size = function () {
    return count;
  };

  return someInstance;
};

메이커 함수 Stack 을 이용해 새로운 인스턴스 stack1stack2 를 생성하면, 리턴한 객체에 담아 둔 push() 메서드를 stack1stack2 가 사용할 수 있다.

const stack1 = Stack();
stack1.push('stack1 push');
// 'stack1 push'라는 문자열이 stack1의 storage객체에 담긴다.

const stack2 = Stack();
stack2.push('stack2 push');
// 'stack2 push'라는 문자열이 stack2의 storage객체에 담긴다.

console.dir(stack1);
// Object
//   pop: ƒ ()
//   push: ƒ (value)
//     [[Scopes]]: Scopes[3]
//       0: Closure (Stack) {storage: {…}, count: 1}
//       ...
//   size: ƒ ()
//   ...

// push()가 접근할 수 있는 객체 storage는 push()의 scope, 그 중 closure에 들어가 있다.
// stack1이 Stack()으로부터 객체를 리턴받아 생성될 때, 그 시점에 고정된 Stack() 내부 환경을 
// 저장하고 있다고 이해했다.

functional 스타일로 만들어지는 인스턴스는 고유한 메모리를 새로 점유하여 만들어진다. stack1stack2 는 모두 push() 라는 같은 메서드를 가지고 있지만, stack1push()stack2push() 는 서로 다른 주소에 저장되어 있다. 그래서 인스턴스를 많이 만들수록 차지하는 메모리가 정비례로 늘어난다.

2. Functional Shared

인스턴스들이 사용하는 메서드를 인스턴스마다 각각 다른 주소에 저장하지 않고, 같은 주소에 담긴 메서드를 가르키도록 하기 위해 고안된 방법이다.
메이커 함수에는 속성을 지닌 객체 하나를 만든다. 그리고 메이커 함수 바깥에 메서드만을 담은 객체를 따로 만든다. 그리고 메이커 함수가 리턴하는 내부 객체에 메서드를 담은 객체의 값을 모두 넣어주는 함수를 만든다.

이렇게 만들어진 인스턴스는 속성은 모두 새로운 메모리를 차지하여 사용하지만, 메서드들은 모든 인스턴스들이 같은 주소에 저장되어 있는 메서드를 사용하므로 인스턴스를 많이 만들어도 Funtional 스타일보다 메모리가 절약된다.

// 메서드만을 담은 함수와, 메이커 함수가 리턴하는 객체를 연결하는 함수
const extend = function (to, from) {
  for (let key in from) {
    to[key] = from[key];
  }
}

const Stack = function () {
  const someInstance = {};
  someInstance.storage = {};
  someInstance.count = 0;

  extend(someInstance, stackMethods);

  return someInstance;
};

const stackMethods = {};

stackMethods.push = function (value) {
  this.storage[this.count] = value;
  this.count++;
}

stackMethods.pop = function () {
  if (this.count === 0) return;
  this.count--;
  return this.storage[this.count];
} 

stackMethods.size = function () {
  return this.count;
}

3. Prototypal

프로토타입 체인을 이용하는 방법이다. Object.creat() 에 매개변수로 함수만을 모아놓은 객체를 전달하여 새로운 객체를 생성하면, 새로운 객체의 __proto__ 는 함수만을 모아놓은 객체를 참조하게 된다.

const Stack = function() {
  const someInstance = Object.create(stackMethods);
  someInstance.storage = {};
  someInstance.count = 0;

  return someInstance;
};

const stackMethods = {};

stackMethods.push = function (value) {
  this.storage[this.count] = value;
  this.count++;
}

stackMethods.pop = function () {
  if (this.count === 0) return;
  this.count--;
  return this.storage[this.count];
}

stackMethods.size = function () {
  return this.count;
}

인스턴스를 생성한 후 개발자 도구에서 인스턴스를 들여다보면, __proto__ 안에 Object.create() 를 사용해 참조하도록 한 객체가 가진 속성과 메서드가 나열되어있다.

const stack1 = Stack();

console.dir(stack1);
// {storage: {…}, count: 0}
// count: 0
// storage: {}
// __proto__:
//    pop: ƒ ()
//    push: ƒ (value)
//    size: ƒ ()
//    __proto__: Object

4. Pseudoclassical

[ new operator | MDN ]
Prototypal 스타일과 마찬가지로 프로토타입 체인을 이용하는 방법이다. 생성자 함수의 prototype 객체에, 공통으로 사용할 메서드를 넣어놓는다. 그러면 생성자 함수를 사용해 만들어진 인스턴스들은 __proto__ 객체를 경유해 생성자 함수의 prototype 객체에 접근, 메서드들을 사용할 수 있게 된다. new 키워드가 객체 설정 및 리턴 없이 인스턴스를 생성하도록 돕는다.

const Stack = function() {
  this.storage = {};
  this.count = 0;
};

Stack.prototype.push = function (value) {
  this.storage[this.count] = value;
  this.count++;
}

Stack.prototype.pop = function () {
  if (this.count === 0) return;
  this.count--;
  return this.storage[this.count];
}

Stack.prototype.size = function () {
  return this.count;
}

// 인스턴스 생성
const stack1 = new Stack();