NHN FORWARD 2021 세션 정리

December 15, 2021

NHN FORWARD 2021 세션 정리

NHN FORWARD 2021 세션 정리

결국 자바스크립트를 알아보기로 했다.

이진우 개발자님

자바스크립트를 알아보게 된 이유

변화하는 개발 환경으로 고민이 생김

“매번 새로운 언어를 배우는 기분”

  • 대부분 프런트엔드 프레임워크 삼대장인 Angular, React, Vue를 사용하여 개발을 하고 있다.
  • 새로운 후발주자들도 계속해서 나오고 있다.(Preact,Svelte,Alpine.js)

고민의 결론

  • 결국 다 자바스크립트로 만들어진 것이다.
  • 자바스크립트를 제대로 알아보자.
  • 새로운 프레임워크를 배우더라도 조금 더 효율적으로 받아들일 수 있을 것이다.

하지만 생각만큼 쉽지 않았다

자바스크립트 공부가 어려웠던 이유

  • 과거엔 현상적으로만 자바스크립트를 풀이해주는 도서들이 많았다.
  • 용어의 정의가 책마다 다 달랐다.
  • 내가 이해할 수 있는 자료를 찾다 보니 단발적으로 정의만 암기할 뿐 전체적인 개념이 잡힌 기분이 아니었다.
  • 구글링으로만 검색하는 것도 한계가 있었다. (자료의 품질을 판단할 능력이 없었다.)

그렇게 헤매다 한 줄기 빛이 내려오는데..

실행 컨텍스트

  • 자바스크립트 동작 원리의 가장 기본인 실행 컨텍스트의 존재를 알게 되었다.
  • 실행 컨텍스트를 중심으로 바라보니 흐름이 잡히기 시작했다.

    • 엔진이 보여지는 코드를 처리하는 방법을 이해할 수 있었다.

자바스크립트 코어를 공부한 뒤 달라진 점

  • 코드를 읽으면서 어떻게 동작할지 예측이 쉬워졌다.

    • 특히 비동기 코드 이해도가 많이 올라갔다.
    • 사용하는 프레임워크나 라이브러리가 생각해도 동작하지 않을 때 구현체를 확인해 볼 용기가 조금 생겼다.
  • 자바스크립트 기술 도서를 읽기가 편해졌다.

    • 다양한 해석을 받아들이기가 수월해졌다.
  • 프레임워크에 종속되지 않는다..(는 기분)

그렇게 정리된 흐름

그렇게 정리된 흐름

자바스크립트 기초 한눈에 보기

알아보기에 앞서

  • 실행 컨텍스트 위주의 동작 방식을 기반으로 진행한다.
  • 정확한 용어, 절차보단 전체적인 컨셉을 이해하는데 집중한다.

1. 실행 컨텍스트(Execution Context)

  • 코드의 실제 진행 상황을 추적하는데 필요한 정보들을 모아둔 구조이다.
  • 함수가 ()로 호출되면 실행 컨텍스트가 새롭게 만들어진다.
  • 함수가 종료되면 실행 컨텍스트는 사라진다.

예제를 보고 흐름을 순서대로 설명해보자!

function sum(a, b) {
  let result = a + b;
  return result;
}

let number = sum(1, 2);
  1. 자바스크립트가 로드되고 엔진이 이를 처리하면서 실행 컨텍스트라는 것을 만든다.
    어떤 코드를 실행하고 있는지 그리고 이 컨텍스트엔 어떤 변수들이 있는지 알기 위한 정보들이 있다.
  2. 전역 실행 컨텍스트와 그에 딸린 전역 메모리가 생성된다.
  3. 코드를 한 줄 한 줄 실행한다.
  4. 전역에 선언된 함수나 변수들을 이 전역 실행 컨텍스트의 메모리에 등록한다.
  5. 전역 메모리에 sum이라는 라벨로 함수 바디를 연결시켜준다.
  6. number라는 변수를 만들어준다.
    이 값은 sum의 결괏값이 될 것이다.
  7. 함수명 뒤에 () 소괄호를 붙여서 함수를 호출한다.
  8. 함수의 실행 컨텍스트가 만들어진다.
    모양은 전역 실행 컨텍스트와 동일하다.
  9. 매개변수를 할당한다.
  10. a라는 매개변수에 1 b라는 매개변수에 2가 할당된다.
  11. result라는 변수를 만든다.
  12. a + b를 연산해서 3이라는 결과를 result에 할당한다.
  13. result를 반환한다.
  14. result값이 number에 할당된다.
  15. sum 함수가 종료되면 함수의 실행 컨텍스트와 그 내부 정보들은 사라진다.

자바스크립트의 함수 객체

  • 서브루틴으로 수행될 수 있는 객체이다.
  • 동작을 나타내는 실행 코드가 있으면서, (Dot Notation을 통해) 일반 객체와 동일하게 동작할 수 있다.

    • 서브루틴으로 실행 예) sum()
    • 일반 객체처럼 동작 예) sum.a = 1;

2. 콜 스택(Call stack)

  • 현재 실행되고 있는 실행 컨텍스트를 추적하기 위한 구조체이다.
  • 콜 스택의 바닥엔 전역 컨텍스트가 존재한다.
  • 함수가 호출될 때 해당 함수의 실행 컨텍스트가 Push되고 함수가 종료되면 Pop된다.

예제를 보고 흐름을 순서대로 설명해보자!

function sum(c, d) {
  let r = c + d;
  return r;
}

function calc(a, b, expr) {
  let result = expr(a, b);
  return result;
}

let number = calc(1, 2, sum);
  1. sum이라는 라벨에 함수 바디를 연결한다.
  2. calc를 선언한다.
  3. calc라는 라벨에 함수 바디를 연결한다.
  4. number 변수를 만든다.
    값은 calc의 리턴이다.
  5. calc를 소괄호로 호출한다.
  6. 함수의 실행 컨텍스트가 새로 만들어진다.
  7. calc라는 함수 실행 컨텍스트가 콜 스택에 쌓인다.
    엔진은 현재 내가 실행하고 있는 실행 컨텍스트가 뭔지 콜 스택의 top을 통해 인지한다.
  8. a, b, expr이라는 매개변수에 각 값을 연결해준다.
  9. a는 1, b는 2, expr은 sum 함수 바디가 된다.
  10. 함수의 내부 코드를 실행한다.
  11. result라는 변수를 만들고 그 값은 expr의 결과가 된다.
  12. expr 또한 괄호를 만나 호출된다.
  13. expr의 실행 컨텍스트가 만들어진다.
  14. 콜 스택에도 expr 컨텍스트가 push 된다.
  15. 매개 변수 c, d에 각각 1,2를 할당한다.
  16. r이라는 변수를 만든다.
  17. r이라는 변수에 c+d를 연산해서 3이 할당된다.
  18. r값 3이 반환되면 result에 할당된다.
  19. expr의 실행 컨텍스트가 종료된다.
  20. 실행 컨텍스트의 종료에 맞춰 콜 스택에서도 사라진다.
  21. result가 반환되어 3이 number에 할당된다.
  22. calc의 실행 컨텍스트가 종료된다.
  23. calc의 콜 스택도 실행 컨텍스트의 종료에 맞춰 사라진다.

3. 스코프(Scope)

  • 현재 접근할 수 있는 변수들의 범위이다.
    크게보면 실행 컨텍스트와 같이 있는 메모리라고 생각하면 된다.
  • 현재 실행 중인 실행 컨텍스트에서 변수를 찾을 수 없다면, 이전 실행 컨텍스트로 탐색 범위를 옮긴다.

    • 이를 스코프 체인(Scope Chain) 이라고 한다.

스코프 체인

이번 예제에서는 변수 선언 키워드를 var로 진행한다.

var spreadRatio = 1.25;

function getMortageRatio(fb) {
  var total = spreadRatio + fb;
  return total;
}

var ratio = getMortageRatio(2);
  1. 전역 메모리에 spreadRatio라는 변수에 1.25를 할당한다.
  2. getMortageRatio라는 라벨에 함수 바디를 연결한다.
  3. ratio라는 변수를 만든다.
    이 값은 getMortageRatio의 리턴이 된다.
  4. getMortageRatio를 호출하면 실행 컨텍스트를 만들고 fb 매개변수에 2라는 값이 할당된다.
  5. total을 만들어준다.
  6. spreadRatio와 fb를 더한 값을 total에 할당한다.
    spreadRatio가 함수 실행 컨텍스트의 메모리에 없기때문에 콜 스택의 바닥인 전역 메모리로 넘어간다.
  7. total을 리턴하여 ratio에 할당한다.
  8. 함수 실행 컨텍스트가 종료된다.

4. 클로저(Closure)

  • 함수가 함수를 반환할 때, 반환되는 함수는 자신을 둘러싼 메모리 환경을 가지고 반환된다.

    • 함수의 호출이 아닌 정의된 위치에 결정되는 스코프이며 이를 렉시컬 스코프라 한다.
  • 상위 실행 컨텍스트로 스코프 체인을 이어가기 전 클로저에 변수가 있는지 먼저 확인한다.
  • 클로저 변수는 함수가 호출이 되어야만 접근이 가능하기 때문에 정보 은닉에 활용된다.
  • 함수 호출 간 공유 메모리로도 활용이 가능하다.

    • 일반적으로 함수가 종료되면 메모리 환경이 사라져, 각 호출 간 연결고리가 없다.
function outer() {
  var n = 0;
  function increase() {
    n += 1;
  }

  return increase;
}

var newFn = outer();
newFn();
newFn();
  1. outer라는 라벨에 함수 바디를 연결한다.
  2. newFn 변수를 만든다.
    값은 outer의 리턴이다.
  3. outer를 호출한다.
  4. outer의 실행 컨텍스트가 만들어지고 콜 스택에 outer가 push된다.
    매개변수가 없으니 바로 코드를 실행한다.
  5. n이 0으로 할당된다.
  6. increase라는 라벨에 함수 바디가 연결된다.
    본체 자체를 반환한다.
  7. newFn이라는 라벨에 똑같은 함수 바디가 연결된다.
  8. outer가 종료되면서 실행 컨텍스트가 날아간다.
  9. newFn을 ()로 실행하여 함수로 호출한다.
    함수 바디로 오니 n에 누적 연산을 한다.
    함수 실행 컨텍스트의 지역 메모리에는 n이 없다.
  10. 앞서 배운 것처럼 콜 스택을 따라 전역 메모리로 올라간다.
    전역 메모리에도 없다.
    n은 사실 outer의 실행 컨텍스트가 종료되면서 사라져버렸다.
    그럼 n은 어디서 찾아야 할까?<br/ > increase가 반환될 때, n을 가지고 반환한다.
    다른 주머니에 보관해서 가지고 오는 것.
    increase가 반환될 때 increase를 둘러싼 메모리 환경을 가지고 나오는 것.

    엔진은 지역 메모리에서 값을 찾지 못했을 경우 상위 컨텍스트로 스코프 체이닝을 이어가는데 그전에 클로저 주머니를 잠깐 확인하고 넘어가는 것.
  11. 그럼 n이 있으니 n을 1로 만들어 줄 수 있다.
  12. 실행 컨텍스트가 종료되고 콜 스택도 비워진다.
  13. 다시 newFn을 호출하면?
  14. 새로운 함수 실행 컨텍스트가 만들어지고 콜 스택에도 push 된다.
    이번에도 n이 지역 메모리에 없다.
    엔진이 상위 컨텍스트로 가야 하는데 그전에 보는건 클로저 주머니이다.
  15. 클로저 주머니에 n이 있으므로 2로 만들어준다.
  16. 함수가 종료되면 실행 컨텍스트가 날아가고 콜 스택도 비워진다.

결국 n은 newFn이 호출되어야만 접근이 가능한 변수가 된다.

5. 비동기 자바스크립트(Asynchronous JavaScript)

  • 자바스크립트 엔진은 기본적으로 싱글 스레드이다.
  • 개발자들은 브라우저 API를 활용하여 비동기 프로그래밍을 할 수 있다.
  • 브라우저 API에서 콜백을 스레드 큐에 등록시킨다.
  • 엔진이 콜백을 실행할 준비가 되면 스레드 큐에서 콜 스택으로 콜백 함수를 넘겨준다.
  • 콜백을 실행할 준비가 되는 시점은

    • 콜 스택이 비어 있고
    • 전역 실행 컨텍스트에서 실행할 코드가 없을 때

    이렇게 조건을 계속 체크하고 콜백 함수를 큐에서 스택으로 옮겨주는 걸 이벤트 루프라 한다.

function greet() {
  console.log("Hi");
}

function wait(ms) {
  // blocking for ms
}

setTimeout(greet, 5);
wait(1000);
console.log("Bye");

각 태스크의 최소 처리 시간은 1밀리 세컨드로 간주한다.

  1. greet라는 라벨에 함수 바디를 연결한다.
  2. 1밀리 세컨드가 흐르고 wait라는 함수가 선언된다.
    이 함수는 매개 변수 만큼의 밀리 세컨드 시간을 끌어주는 함수이다.
    (이 함수를 선언하는 데까지 2밀리 세컨드가 걸린다.)
  3. setTimeout이라는 브라우저 API를 호출한다.
    첫 번째 매개 변수는 콜백 함수고, 두 번째는 지연 시간이다. (setTimeout을 호출해 주는 데까지 3밀리 세컨드가 걸린다.)
  4. setTimeout의 호출은 종료된다.
    브라우저 API 에 타이머가 하나 생기고 그 콜백으로 greet가 등록되어 있게 된다.
  5. wait이 호출된다.
  6. 함수 실행 컨텍스트가 만들어지고 매개 변수 ms에 1000을 할당한다.
    실제 함수 바디를 수행하면서 1000밀리 세컨드만큼 끌어준다.
    (여기까지 5밀리 세컨드가 걸린다.)
  7. 8밀리 세컨드가 지났을 때 3밀리 세컨드에서 만들어진 타이머가 종료된다.
    브라우저는 해당 타이머의 콜백 함수를 스레드 큐라는 큐에 보내서 잠시 대기시킨다.
    실제로 엔진에선 wait을 열심히 실행중이다.
    (여기까지 9밀리 세컨드가 걸린다.)
  8. 1000밀리 세컨드의 작업을 마치고 실행 컨텍스트가 종료된다.
  9. 콘솔에 ‘Bye’를 남긴다.
  10. 엔진은 큐에서 콜 스택으로 콜백 함수를 옮겨준다.
    (여기까지 1006밀리 세컨드가 걸린다.)
  11. 함수 실행 컨텍스트를 만들면서 콘솔에 ‘Hi’를 남긴다.
  12. 함수 실행 컨텍스트가 종료된다.

6. 자바스크립트 프로토타입(JavaScript Prototype)

  • 자바스크립트는 프로토타입이라는 특수한 객체를 가지고 있다.
  • 인스턴스 생성을 위해 (this라는 이름으로) 빈 객체를 만들고
  • 프로토타입을 연결해준 뒤
  • 속성 값들을 할당해주고
  • 반환
let fns = {
  getName() {
    return this.name;
  },
  addAge() {
    this.age += 1;
  },
};
function createPerson(name) {
  let newPerson = {};
  Object.setPrototypeOf(newPerson, fns);
  newPerson.name = name;
  newPerson.age = 0;
  return newPerson;
}

let john = createPerson("john");

위 코드는 프로토 타입으로 인스턴스 객체를 만들어내는 코드다.

  1. fns라는 라벨을 만들고 객체 전체를 연결한다.
  2. createPerson 라벨에 함수 바디를 연결해준다.
  3. john이란 변수를 만든다.
    할당되는 값은 createPerson의 리턴이다.
  4. 함수를 호출한다.
  5. 실행 컨텍스트가 만들어진다.
  6. name이란 매개 변수에 john이 할당된다.
  7. 바디 코드가 실행된다.
  8. newPerson에 빈 객체를 할당한다.
  9. Object.setPrototypeOf라는 함수를 통해 newPerson의 프로토타입을 fns 객체로 연결해준다.
    이제 newPerson의 프로토타입은 fns 객체가 된다.
  10. fns를 참조하기 위한 라벨이 필요한데 __proto__라는 특수 속성에 붙여준다.
  11. newPerson에 name 속성을 만들고 매개변수 name을 할당한다. name의 값은 john이 된다.
  12. age 속성을 만들고 0을 할당해준다. 이제 newPerson의 모습은 name과 age속성을 가지고 있고 __proto__라는 속성을 통해 fns로의 연결을 가진다.
  13. newPerson 객체가 반환되어 john에 할당된다.
  14. createPerson 함수가 종료된다. 실행 컨텍스트가 날아간다.
  15. john에 getName이 있는지 찾는다. 객체 자체에는 getName이 없기때문에 엔진은 __proto__를 참조한다. __proto__는 fns로 연결해주고 fns에는 getName이 있다. getName은 함수이니 괄호로 호출이 가능하다.
  16. 다시 새로운 실행 컨텍스트가 만들어지고 이름을 반환하며 종료된다.
function createPerson(name) {
  // let newPerson = {};
  // Object.setPrototypeOf(
  //   newPerson, fns
  // );
  // newPerson.name = name;
  this.name = name;
  // newPerson.age = 0;
  this.age = 0;
  // return newPerson;
}

createPerson.prototype.getName = function () {
  return this.name;
};
createPerson.prototype.addAge = function () {
  this.age += 1;
};

let john = new createPerson("john");
john.getName();
  1. 위 코드는 new로 인서턴스를 만드는 코드이다.
  2. createPerson 라벨에 함수 바디를 연결한다.

createPerson에 점을 붙여 객체처럼 동작시킨다.
이 객체 파드엔 프로토타입이라는 빈 객체가 있다.

  1. 이 객체에 getName과 addAge란 속성을 추가하고 각 함수 바디를 연결한다.
  2. john이란 변수를 만든다.
    값은 createPerson의 리턴이 된다.
  3. createPerson을 소괄호를 이용하여 함수로 호출하는데 new를 붙여서 호출한다.
    실행 컨텍스트가 만들어진다.
  4. 매개 변수 name에 john이 할당된다.

빈 객체를 만들고 여기에 ‘this’라고 이름을 붙인다. 이 객체의 프로토 타입을 createPerson.prototype으로 맞춰준다.

  1. name 속성에 name 매개변수를 할당한다.
  2. age속성에 0을 할당한다.

다루지 못한 컨셉들

  • 자바스크립트에서 상속을 구현하는 방법

    • 프로토타입
  • ES2015+ 문법들의 동작 원리

    • (class, super, extends, Promise, Iterator, Generator, Async/Await)
    • 스레드 큐의 두 가지 종류(MicroTask, MacroTask)

자바스크립트 상속

  • 프로토타입이 부모-자식 간 어떻게 연결이 되는지

    • 프로토타입 또한 객체이기에 proto를 갖고 있다.

ES2015+ 스펙 중 주요 문법들

  • class, super, extends 키워드

    • 결국 프로토타입으로 만들어진다.
  • Promise

    • 비동기 처리 방법을 그대로 따라간다.
    • MicroTask, MacroTask를 알아보자.
  • ES2016의 async/await

    • ES2015의 Promise, iterator, generator를 알면 더 확실히 보인다.

이제부터는?

  • 어떤 개념의 정의를 무작정 외우기보단 전체적인 컨셉을 먼저 알아본다.
  • 편의 문법(Sugar Syntax)을 적극적으로 활용하되, 내부 동작을 이해하고 쓴다.

출처

https://forward.nhn.com/2021/sessions/17


Written by @Soojin Kim

넥스트이노베이션의 기술, 문화, 뉴스, 행사 등 최신 소식 및 넥스트이노베이션이 겪은 다양한 경험을 공유합니다.