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);
- 자바스크립트가 로드되고 엔진이 이를 처리하면서 실행 컨텍스트라는 것을 만든다.
어떤 코드를 실행하고 있는지 그리고 이 컨텍스트엔 어떤 변수들이 있는지 알기 위한 정보들이 있다. - 전역 실행 컨텍스트와 그에 딸린 전역 메모리가 생성된다.
- 코드를 한 줄 한 줄 실행한다.
- 전역에 선언된 함수나 변수들을 이 전역 실행 컨텍스트의 메모리에 등록한다.
- 전역 메모리에 sum이라는 라벨로 함수 바디를 연결시켜준다.
- number라는 변수를 만들어준다.
이 값은 sum의 결괏값이 될 것이다. - 함수명 뒤에 () 소괄호를 붙여서 함수를 호출한다.
- 함수의 실행 컨텍스트가 만들어진다.
모양은 전역 실행 컨텍스트와 동일하다. - 매개변수를 할당한다.
- a라는 매개변수에 1 b라는 매개변수에 2가 할당된다.
- result라는 변수를 만든다.
- a + b를 연산해서 3이라는 결과를 result에 할당한다.
- result를 반환한다.
- result값이 number에 할당된다.
- 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);
- sum이라는 라벨에 함수 바디를 연결한다.
- calc를 선언한다.
- calc라는 라벨에 함수 바디를 연결한다.
- number 변수를 만든다.
값은 calc의 리턴이다. - calc를 소괄호로 호출한다.
- 함수의 실행 컨텍스트가 새로 만들어진다.
- calc라는 함수 실행 컨텍스트가 콜 스택에 쌓인다.
엔진은 현재 내가 실행하고 있는 실행 컨텍스트가 뭔지 콜 스택의 top을 통해 인지한다. - a, b, expr이라는 매개변수에 각 값을 연결해준다.
- a는 1, b는 2, expr은 sum 함수 바디가 된다.
- 함수의 내부 코드를 실행한다.
- result라는 변수를 만들고 그 값은 expr의 결과가 된다.
- expr 또한 괄호를 만나 호출된다.
- expr의 실행 컨텍스트가 만들어진다.
- 콜 스택에도 expr 컨텍스트가 push 된다.
- 매개 변수 c, d에 각각 1,2를 할당한다.
- r이라는 변수를 만든다.
- r이라는 변수에 c+d를 연산해서 3이 할당된다.
- r값 3이 반환되면 result에 할당된다.
- expr의 실행 컨텍스트가 종료된다.
- 실행 컨텍스트의 종료에 맞춰 콜 스택에서도 사라진다.
- result가 반환되어 3이 number에 할당된다.
- calc의 실행 컨텍스트가 종료된다.
- calc의 콜 스택도 실행 컨텍스트의 종료에 맞춰 사라진다.
3. 스코프(Scope)
- 현재 접근할 수 있는 변수들의 범위이다.
크게보면 실행 컨텍스트와 같이 있는 메모리라고 생각하면 된다. -
현재 실행 중인 실행 컨텍스트에서 변수를 찾을 수 없다면, 이전 실행 컨텍스트로 탐색 범위를 옮긴다.
- 이를 스코프 체인(Scope Chain) 이라고 한다.
이번 예제에서는 변수 선언 키워드를 var로 진행한다.
var spreadRatio = 1.25;
function getMortageRatio(fb) {
var total = spreadRatio + fb;
return total;
}
var ratio = getMortageRatio(2);
- 전역 메모리에 spreadRatio라는 변수에 1.25를 할당한다.
- getMortageRatio라는 라벨에 함수 바디를 연결한다.
- ratio라는 변수를 만든다.
이 값은 getMortageRatio의 리턴이 된다. - getMortageRatio를 호출하면 실행 컨텍스트를 만들고 fb 매개변수에 2라는 값이 할당된다.
- total을 만들어준다.
- spreadRatio와 fb를 더한 값을 total에 할당한다.
spreadRatio가 함수 실행 컨텍스트의 메모리에 없기때문에 콜 스택의 바닥인 전역 메모리로 넘어간다. - total을 리턴하여 ratio에 할당한다.
- 함수 실행 컨텍스트가 종료된다.
4. 클로저(Closure)
-
함수가 함수를 반환할 때, 반환되는 함수는 자신을 둘러싼 메모리 환경을 가지고 반환된다.
- 함수의 호출이 아닌 정의된 위치에 결정되는 스코프이며 이를 렉시컬 스코프라 한다.
- 상위 실행 컨텍스트로 스코프 체인을 이어가기 전 클로저에 변수가 있는지 먼저 확인한다.
- 클로저 변수는 함수가 호출이 되어야만 접근이 가능하기 때문에 정보 은닉에 활용된다.
-
함수 호출 간 공유 메모리로도 활용이 가능하다.
- 일반적으로 함수가 종료되면 메모리 환경이 사라져, 각 호출 간 연결고리가 없다.
function outer() {
var n = 0;
function increase() {
n += 1;
}
return increase;
}
var newFn = outer();
newFn();
newFn();
- outer라는 라벨에 함수 바디를 연결한다.
- newFn 변수를 만든다.
값은 outer의 리턴이다. - outer를 호출한다.
- outer의 실행 컨텍스트가 만들어지고 콜 스택에 outer가 push된다.
매개변수가 없으니 바로 코드를 실행한다. - n이 0으로 할당된다.
- increase라는 라벨에 함수 바디가 연결된다.
본체 자체를 반환한다. - newFn이라는 라벨에 똑같은 함수 바디가 연결된다.
- outer가 종료되면서 실행 컨텍스트가 날아간다.
- newFn을 ()로 실행하여 함수로 호출한다.
함수 바디로 오니 n에 누적 연산을 한다.
함수 실행 컨텍스트의 지역 메모리에는 n이 없다. - 앞서 배운 것처럼 콜 스택을 따라 전역 메모리로 올라간다.
전역 메모리에도 없다.
n은 사실 outer의 실행 컨텍스트가 종료되면서 사라져버렸다.
그럼 n은 어디서 찾아야 할까?<br/ > increase가 반환될 때, n을 가지고 반환한다.
다른 주머니에 보관해서 가지고 오는 것.
increase가 반환될 때 increase를 둘러싼 메모리 환경을 가지고 나오는 것.
엔진은 지역 메모리에서 값을 찾지 못했을 경우 상위 컨텍스트로 스코프 체이닝을 이어가는데 그전에 클로저 주머니를 잠깐 확인하고 넘어가는 것. - 그럼 n이 있으니 n을 1로 만들어 줄 수 있다.
- 실행 컨텍스트가 종료되고 콜 스택도 비워진다.
- 다시 newFn을 호출하면?
- 새로운 함수 실행 컨텍스트가 만들어지고 콜 스택에도 push 된다.
이번에도 n이 지역 메모리에 없다.
엔진이 상위 컨텍스트로 가야 하는데 그전에 보는건 클로저 주머니이다. - 클로저 주머니에 n이 있으므로 2로 만들어준다.
- 함수가 종료되면 실행 컨텍스트가 날아가고 콜 스택도 비워진다.
결국 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밀리 세컨드로 간주한다.
- greet라는 라벨에 함수 바디를 연결한다.
- 1밀리 세컨드가 흐르고 wait라는 함수가 선언된다.
이 함수는 매개 변수 만큼의 밀리 세컨드 시간을 끌어주는 함수이다.
(이 함수를 선언하는 데까지 2밀리 세컨드가 걸린다.) - setTimeout이라는 브라우저 API를 호출한다.
첫 번째 매개 변수는 콜백 함수고, 두 번째는 지연 시간이다. (setTimeout을 호출해 주는 데까지 3밀리 세컨드가 걸린다.) - setTimeout의 호출은 종료된다.
브라우저 API 에 타이머가 하나 생기고 그 콜백으로 greet가 등록되어 있게 된다. - wait이 호출된다.
- 함수 실행 컨텍스트가 만들어지고 매개 변수 ms에 1000을 할당한다.
실제 함수 바디를 수행하면서 1000밀리 세컨드만큼 끌어준다.
(여기까지 5밀리 세컨드가 걸린다.) - 8밀리 세컨드가 지났을 때 3밀리 세컨드에서 만들어진 타이머가 종료된다.
브라우저는 해당 타이머의 콜백 함수를 스레드 큐라는 큐에 보내서 잠시 대기시킨다.
실제로 엔진에선 wait을 열심히 실행중이다.
(여기까지 9밀리 세컨드가 걸린다.) - 1000밀리 세컨드의 작업을 마치고 실행 컨텍스트가 종료된다.
- 콘솔에 ‘Bye’를 남긴다.
- 엔진은 큐에서 콜 스택으로 콜백 함수를 옮겨준다.
(여기까지 1006밀리 세컨드가 걸린다.) - 함수 실행 컨텍스트를 만들면서 콘솔에 ‘Hi’를 남긴다.
- 함수 실행 컨텍스트가 종료된다.
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");
위 코드는 프로토 타입으로 인스턴스 객체를 만들어내는 코드다.
- fns라는 라벨을 만들고 객체 전체를 연결한다.
- createPerson 라벨에 함수 바디를 연결해준다.
- john이란 변수를 만든다.
할당되는 값은 createPerson의 리턴이다. - 함수를 호출한다.
- 실행 컨텍스트가 만들어진다.
- name이란 매개 변수에 john이 할당된다.
- 바디 코드가 실행된다.
- newPerson에 빈 객체를 할당한다.
- Object.setPrototypeOf라는 함수를 통해 newPerson의 프로토타입을 fns 객체로 연결해준다.
이제 newPerson의 프로토타입은 fns 객체가 된다. - fns를 참조하기 위한 라벨이 필요한데
__proto__
라는 특수 속성에 붙여준다. - newPerson에 name 속성을 만들고 매개변수 name을 할당한다. name의 값은 john이 된다.
- age 속성을 만들고 0을 할당해준다.
이제 newPerson의 모습은 name과 age속성을 가지고 있고
__proto__
라는 속성을 통해 fns로의 연결을 가진다. - newPerson 객체가 반환되어 john에 할당된다.
- createPerson 함수가 종료된다. 실행 컨텍스트가 날아간다.
- john에 getName이 있는지 찾는다.
객체 자체에는 getName이 없기때문에 엔진은
__proto__
를 참조한다.__proto__
는 fns로 연결해주고 fns에는 getName이 있다. getName은 함수이니 괄호로 호출이 가능하다. - 다시 새로운 실행 컨텍스트가 만들어지고 이름을 반환하며 종료된다.
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();
- 위 코드는 new로 인서턴스를 만드는 코드이다.
- createPerson 라벨에 함수 바디를 연결한다.
createPerson에 점을 붙여 객체처럼 동작시킨다.
이 객체 파드엔 프로토타입이라는 빈 객체가 있다.
- 이 객체에 getName과 addAge란 속성을 추가하고 각 함수 바디를 연결한다.
- john이란 변수를 만든다.
값은 createPerson의 리턴이 된다. - createPerson을 소괄호를 이용하여 함수로 호출하는데 new를 붙여서 호출한다.
실행 컨텍스트가 만들어진다. - 매개 변수 name에 john이 할당된다.
빈 객체를 만들고 여기에 ‘this’라고 이름을 붙인다. 이 객체의 프로토 타입을 createPerson.prototype으로 맞춰준다.
- name 속성에 name 매개변수를 할당한다.
- 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)을 적극적으로 활용하되, 내부 동작을 이해하고 쓴다.