[CodeStates] Week 2-2 : Sprint - JavaScript Koans

Bare Minimum Requirements

브라우저에서 KoansRunner.html을 열어보면 확인할 수 있는 통과하지 못한 테스트가 여럿 들어있습니다. 테스트 파일들은 /koans에 있습니다. koans/01_Introduction.js 부터 문제를 풀어주시면 되겠습니다. 이 파일들의 모든 테스트가 통과되도록 하세요. 문법 에러는 개발자 콘솔 창에서 확인할 수 있습니다.

풀이

01_Introduction.js

본 스프린트에서는 테스트하는 값과 기대값을 비교하기 위해 expect 함수를 사용하고 있다.
mocha, chai 등 일부 matcher 라이브러리를 사용하여 진행된다.
여기서는 expect 함수를 어떻게 사용하는지 그 사용법을 나타내고 있다.

expect(false).to.be.true;

여기서 expect는 기대값이며 to.be. 다음에 작성되는 항목이 목표값이 된다.
즉, 목표값과 동일하도록 기대값을 변경한 뒤 KoansRunner.html을 실행하면 해결된다.

이처럼 to.be.true, to.be.false 만을 가지고도 많은 테스트 케이스를 작성할 수 있다.
하지만 이는 직관적이지 않고 다소 불편하다고 느낄 수 있는데, 두 값 A와 B를 '비교한 결과'가 참인지를 확인하는 대신에 직접 A가 B와 같은지 확인하는 matcher가 존재한다. .equal이 바로 그런 역할을 한다.

expect('테스트하는값').to.equal('기대하는값');

02_Types-part1.js

  • 비교 연산자 ==는 느슨한 동치 연산을 수행하기 때문에 (타입 변환이 일어남) ===을 사용하여 엄격한 동치 연산을 수행해야 한다.

따라서, ==의 별난 특성들을 전부 외워서 모든 상황에 대응하려고 하지 말고, 올바른 코딩 습관을 길러야 함을 문제를 통해서 당부하고 있다. 대표적으로 최대한 같은 타입끼리 연산을 하고, 즉 엄격한 동치 연산('===')을 사용하고, 조건문에 비교 연산을 명시하는 것이 훨씬 좋다.

03_LetConst.js

  • const는 재할당이 금지되므로, 재할당 구문을 주석처리하면 해결됨
  • const로 선언된 배열, 객체는 요소의 추가 및 삭제가 가능함

'let' 키워드는 재할당이 가능하기 때문에 여러모로 편하고, 큰 문제도 없어 보이기 때문에 재할당도 안되는 'const' 키워드를 굳이 써야하는지 이해가 안 될수도 있지만 세계의 탑코더들이 있는 구글 스타일 가이드에서 이를 잘 설명하고 있으니 참고하면 좋다.

04_Scope.js

MDN에 따르면 클로저의 정의는 다음과 같다.

  • 내부 함수와 외부 함수의 개념을 이해하면서 expect 구문을 완성하면 쉽게 통과됨

A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time the closure was created.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합을 말한다.
이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.

여기서의 키워드는 "함수가 선언"된 "어휘적(lexical) 환경"이다.
특이하게도 자바스크립트는 함수가 호출되는 환경와 별개로, 기존에 선언되어 있던 환경 - 어휘적 환경 - 을 기준으로 변수를 조회하려고 한다. 클로저는 내부(inner) 함수가 외부(outer) 함수의 지역 변수에 접근할 수 있다!

스프린트 중간에 호이스팅(hoisting)을 다루고 있는데 해당 개념을 간단하게 요약하면 다음과 같다.

  • 자바스크립트에는 호이스팅이라는게 있어서 변수가 소스 위쪽으로 올라가는데 스코프에 따라 올라가는 정도가 나누어 진다.
  • 여러가지 스코프가 있겠지만 보통적으로는 펑션스코프와 함수 스코프가있는데 var는 펑션 스코프고 let,const는 함수 스코프이다.
  • 스코프 앞에 있는 것 까지 호이스팅되서 그렇다!

다만, 호이스팅이 단순히 끌어올리는 것이라고 생각해서는 안된다. 호이스팅은 자바스크립트의 구동 방식이 끌어올리는 것처럼 유사하게 보여 표현한 것이며 실제로는 끌어올리는 것이 아니다. 이를 이해하기 위해서는 실행 컨텍스트를 이해해야 한다.

실행 컨텍스트

실행 컨텍스트: 실행할 코드에 제공할 환경 정보, 변수를 모아놓은 객체

아래의 코드를 활용하여 이해를 돕는다.

var a = 1;					//1
function outer() {			//2
  function inner() {		//3
    console.log(a);			//4
    var a = 3;				//5
  }							//6
  inner();					//7
  console.log(a);			//8
}							//9
outer();					//10
console.log(a);				//11

JavaScript는 스택에 함수가 쌓여서 실행된다.

  • (1) -> (2): JavaScript 파일이 열리면서 전역 컨텍스트가 생성
  • (2) -> (3): 10번째 줄에서 outer()를 실행하여 outer 실행 컨텍스트가 스택에 쌓임
  • (3) -> (4): outer 함수 내부의 inner()가 실행되며 inner 실행 컨텍스트가 스택에 쌓임
  • (4) -> (5): inner 함수가 6번째 줄에서 실행 완료되고 스택에서 inner 실행 컨텍스트를 삭제. 그리고 8번째의 console.log(a) 실행
  • (5) -> (6): outer 함수가 8번째 줄에서 실행 완료되고 스택에서 outer 실행 컨텍스트를 삭제. 그리고 11번째의 console.log(a) 실행
  • (6) -> (7): 11번째의 console.log(a)가 완료되면 전역 컨텍스트를 삭제하고 종료

여기서 사용되는 실행 컨텍스트에 VariableEnvironment, LexicalEnvironment, ThisBinding이 들어간다.
출처: 코어 자바스크립트, 정재남 2019

05_ArrowFunction.js

화살표 함수는 Immersive 코스에서 ES6 문법에 대해 더 자세하게 다룰 예정이다.

간략하게, 기존의 함수식을 화살표 함수를 통해 간략하게 작성하여 가독성에 도움을 주는 방식이다.
필요에 따라 리턴을 생략하거나 소괄호를 붙일 수도 혹은 생략이 가능하다.

let add = function (x, y) {
  return x + y;

let add = (x, y) => x + y
let add = (x, y) => {
  return x + y;
}
...
  

06_Types-part2.js

  • 원시 자료형은 값 자체에 대한 변경이 불가능(immutable)함.

    원시 자료형은 값 자체에 대한 변경이 불가능하다고 하는데, 한 변수에 다른 값을 할당하는 것은 변경이 된 것이 아닌지?
    let num1 = 123;
    num2 = 123456;
    원시 자료형 그 자체('hello', 123, 456n, true 등)와 원시 자료형이 할당된 변수는 구분되어야 함. 사과 박스에 귤을 담았다고 해서, 귤이 갑자기 사과가 되지는 않는 것과 같이 123이 갑자기 123456이 되지 않는다.

  • 원시 자료형을 변수에 할당할 경우, 값 자체의 복사가 일어남.
  • 원시 자료형 또는 원시 자료형의 데이터를 함수의 인자로 전달할 경우, 값 자체의 복사가 일어납니다.

사실 함수의 인자도 변수에 자료(data)를 할당하는 것.
함수를 호출하면서 넘긴 인자가 호출된 함수의 지역변수로 (매 호출 시마다) 새롭게 선언됨.

자바스크립트에서 원시 자료형이 아닌 모든 것은 참조 자료형 이다. 배열([])과 객체({}), 함수(function(){})가 대표적.

    const pi = 3.14
    const arr = ["hello", "world", "code", "states"];

위 두 가지 코드에서 보기에는 큰 차이가 없다.
하지만 자바스크립트는 보기와는 다르게 작동되는 부분이 있다. (under the hood)
여기서 변수 pi에는 3.14라는 원시 자료형 '값'이 할당되고, arr에는 참조 자료형의 '주소'가 할당된다.
영어 단어 reference 의미와 연결시켜보면 실제 데이터가 저장된 주소를 가리킨다(refer), 즉, 참조(reference)한다로 이해하면 쉽다.

왜 참조 자료형에서는 '주소'를 할당할 수 밖에 없을까?
원시 자료형은 immutable 하지만, 참조 자료형은 그렇지 않다.

배열에 요소를 추가 및 삭제하고, 객체에 속성을 추가 및 삭제하며 참조 자료형은 이미 immutable하지 않다는 것을 경험하고 있었다.
언제든 데이터가 늘어나고 줄어들 수 있고 (동적으로 변한다.), 그렇기 때문에 특별한 저장공간의 주소를 변수에 할당함으로써 더 잘 관리하는데, 이런 저장 공간을 heap이라고 부른다.

아래와 같이 코드가 작성되어 있다면...

  let num = 123;
  const msg = "hello";
  let arr = [1, 2, 3];
  const isOdd = true;

원시 자료형의 데이터가 저장되는 공간 (stack)

   1 | num |   123
   2 | msg | "hello"
   3 | arr | heap의 12번부터 3// (실제 데이터가 저장되어 있는 주소)
   4 |isOdd|   true
=====================================
Object 자료형의 데이터가 저장되는 공간 (heap)
   10 ||   
   11 || 
   12 || 1
   13 || 2  
   14 || 3  
실제 자바스크립트는 변수를 위와 같이 저장할 것이다.  
  • 참조 자료형을 변수에 할당할 경우, 데이터의 주소가 저장된다.

    참조 자료형의 경우, 값 자체의 복사가 일어나지 않는 이유는 어느 정도 납득할만한 이유가 있다.
    배열이 얼마나 많은 데이터를 가지고 있는지가 프로그램의 실행 중 수시로 변경될 수 있기 때문. 쉽게 생각해서 number 타입 데이터 100만개를 요소로 갖는 배열을 생각해보면, 따로 명시하지 않는 이상 100만개의 데이터를 일일히 복사하는 것은 상당히 비효율적이다. 따라서 일단은 주소만 복사해서 동일한 데이터를 바라보는 게 만드는 것이 효율적임.

  const nums1 = [1, 2, 3];
  const nums2 = [1, 2, 3];
  expect(nums1 === nums2).to.equal(FILL_ME_IN);

배열 nums1과 배열 num2에는 동일한 데이터 [1, 2, 3]이 들어있는 게 분명해 보이는데, 이 둘은 같지가 않다. 사실 변수 num1와 num2는 배열이 아니다.
정확히 말해서 변수 num1은 데이터 [1, 2, 3]이 저장되어 있는 메모리 공간(heap)을 가리키는 주소를 담고 있다.
따라서 위의 코드는 각각 다음의 의미를 가지고 있다.

const nums1 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 이 위치의 주소가 변수 num1에 저장된다.
const nums2 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 이 위치의 주소가 변수 num2에 저장된다.

이제 heap에는 두 개의 [1, 2, 3]이 저장되어 있고, 각각에 대한 주소가 변수 num1, num2에 저장되어 있다. 이러한 방식이 비효율적으로 보일수도 있다.

하지만 [1, 2, 3]이 아니라 상당히 큰 데이터(예. length가 100,000인 배열)를 가지고 다시 생각해보자.

const nums1 = [10, 2, 71, ..., 987]; // 길이 100,000개인 배열
const nums2 = [10, 2, 71, ..., 987]; // 길이 100,000개인 배열

이 두 배열이 서로 같아서 두 번 저장할 필요가 없다고 말하려면, 일단 두 배열이 같은지 확인해야 한다.
이런 작업을 Object 자료형을 쓸 때마다 한다고 가정해보면, 이것이 얼마나 비효율적인지를 금방 알 수 있다.

이제 아래와 같이 정리할 수 있다.
Object 자료형은 데이터는 heap에 저장되고, 변수에 할당을 할 경우 변수에는 주소가 저장된다.

1) [1, 2, 3]; // [1, 2, 3]이라는 데이터가 heap에 저장되지만 변수 할당이 되지 않아 주소는 어디에도 저장되지 않는다.
2) const num1 = [1, 2, 3]; // // [1, 2, 3]이라는 데이터가 heap에 저장되고, 그 주소가 변수 num1에 저장된다.
3) const num2 = [1, 2, 3]; // // [1, 2, 3]이라는 데이터가 heap에 저장되고, 그 주소가 변수 num2에 저장된다.

1), 2), 3)에서 말하는 주소는 전부 다른 주소다. 

07_Array.js

08_Object.js

this란?

    const currentYear = new Date().getFullYear();
    const megalomaniac = {
      mastermind: 'James Wood',
      henchman: 'Adam West',
      birthYear: 1970,
      calculateAge: function (currentYear) {
        return currentYear - this.birthYear;
      },
      changeBirthYear: function (newYear) {
        this.birthYear = newYear;
      },
    };

method는 '어떤 객체의 속성으로 정의된 함수'를 말한다. 위의 megalomaniac 객체를 예로 든다면,
calculateAgemegalomaniac 객체의 속성으로 정의된 함수인 '메소드'라고 할 수 있다. megalomaniac.calculateAge()와 같은 형태로 사용(호출)할 수 있다.
사실은, 전역 변수에 선언한 함수도 웹페이지에서 window 객체의 속성으로 정의된 함수라고 할 수 있다.
window. 접두사 없이도 참조가 가능하기 때문에(window.foo()라고 사용해도 된다.), 생략하고 쓰는 것뿐이다. 이렇듯, method는 항상 '어떤 객체'의 method다.
따라서 호출될 때마다 어떠한 객체의 method일 텐데, 그 '어떠한 객체'를 묻는 것이 this입니다.
예시로, obj이라는 객체 안에 foo라는 메서드를 선언하고, this를 반환한다고 했을 때

let obj = {foo: function() {return this}};

obj.foo() === obj 이라는 코드에 true라고 반환할 것이다.
this는 함수의 호출에 따라서 값이 달라지기도 한다. (apply, call, bind)
그러나 화살표 함수는 자신의 this가 없다.
화살표 함수에서의 this는 자신을 감싼 정적 범위(lexical context)다. (전역에서는 전역 객체를 가리킴.)
일반 변수 조회 규칙(normal variable lookup rules)을 따르기 때문에, 현재 범위에서 존재하지 않는 this를 찾을 때, 화살표 함수 바로 바깥 범위에서 this를 찾는다.
그렇기에 화살표 함수를 사용할 때, 이러한 특이점을 생각하고 사용해야 한다.

  • Object.assign을 통한 복사는 reference variable은 주소만 복사한다.

이와 관련하여 얕은 복사(shallow copy)와 깊은 복사(deep copy)는 링크1, 링크2로 대체한다.

09_SpreadSyntax.js

10_Destructuring.js

좋은 웹페이지 즐겨찾기