서론
프로그래머스 데브코스 11일차의 마지막 파트이다. 이걸 적느라 3일치 분량 정리가 밀렸다니 믿기지 않을 정도다.
일단 어서 파트 3 TIL을 시작하도록 하겠다.
이번 일자의 프로그래머스 데브코스는 자바스크립트 기초 - 객체와 빌트인 객체 그리고 매커니즘이다.
스코프 (Scope)
스코프(Scope)에는 범위 혹은 시야라는 의미가 있다.
이는 변수 접근 규칙에 따른 유효 범위라고 할 수 있다.
더 쉽게 설명하자면 그냥 변수가 어디에서 선언되었으냐에 따른 변수 자체의 유효 범위이다.
이 내용은 사실 9일차 TIL에 이미 한 번 나온바가 있다.
스코프 규칙
// 전역 스코프
var globalVar = "전역";
function outer() {
// outer 함수 스코프
var outerVar = "외부";
function inner() {
// inner 함수 스코프
var innerVar = "내부";
console.log(innerVar); // "내부"
console.log(outerVar); // "외부"
console.log(globalVar); // "전역"
}
inner();
}
outer();
스코프에는 2가지 규칙이 있다. 정적 영역 규칙 (static scoping rule), 동적 영역 규칙 (dynamic scoping rule). 이 중 정적 영역 규칙은 렉시컬 스코프(lexical scoping rule)라고도 한다.
- 정적 영역 규칙 (static scoping rule) or 렉시컬 스코프 (lexical scoping rule)
- 어디서 호출했는지가 아니라, 어디에 선언했는지에 따라 스코프가 결정된다.
- 자바스크립트는 정적 렉시컬 스코프를 따른다.
- 동적 영역 규칙 (dynamic scoping rule)
- 어디서 호출하였는지에 따라 스코프가 결정된다.
- 런타임 시 결정된다.
참고로 자바스크립트를 비롯한 많은 언어들은 렉시컬 스코프를 따른다. 그리고 이러한 내용으로 알 수 있다시피 렉시컬 스코프는 함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정된다.
스코프 종류
전역 스코프
// 전역 스코프
var globalVar = '전역 스코프'; //전역 변수
function exampleFunction() {
// 전역 스코프의 변수에 접근 가능
console.log(globalVar);
}
exampleFunction();
소스 코드상 모든 곳에서 사용할 수 있는 전역 변수이다. (브라우저 기준 window 속성)
파일 스코프
// 파일 스코프
// 이 파일 내에서만 접근 가능
var fileVar = '파일 스코프';
export function getFileVar() {
return fileVar;
}
해당 파일 전체에서 접근 가능하고, 다른 파일에서는 접근이 불가능하다. 원시적인 형태의 모듈 영역이다. ES6+ 자바스크립트에선 <script type="module" ...>로 조회되는 파일이다.
모듈 스코프
//모듈 스코프
var moduleVar = '모듈 스코프';
export function getModuleVar() {
return moduleVar;
}
모듈을 지원하는 프로그래밍 언어에서 모듈 단위 변수 선언이 가능하다. 사실 파일 스코프와 여러모로 비슷하다.
함수 레벨 스코프
// 함수 레벨 스코프
function functionScopeExample() {
var localVar = '함수 레벨 스코프';
if (true) {
// 함수 내부에서 선언된 변수는 블록 안에서도 사용 가능
console.log(localVar);
}
// 함수 내부에서 선언된 변수는 함수 외부에서는 사용 불가능
}
functionScopeExample();
console.log(localVar); // 에러 발생
함수에서 선언한 변수는 해당 함수 내부라면 어디서든 접근 가능하다. 일반적인 의미의 지역 스코프라고 할 수 있다. 함수가 반환된 이후에는 사용이 불가능하며, 함수 외부에서는 호출하지 않는다. 자바스크립트에서는 기본적으로 함수 레벨 스코프이다.
블록 레벨 스코프
// 블록 레벨 스코프
function blockScopeExample() {
if (true) {
let blockVar = '블록 레벨 스코프';
// 블록 내부에서 선언된 변수는 블록 외부에서는 사용 불가능
console.log(blockVar);
}
console.log(blockVar); // 에러 발생
}
blockScopeExample();
함수, if 문, for 문, while 문, try/catch 문 등으로 생성된 블록({}; 중괄호) 내부에서 선언된 변수는 해당 블록에서만 접근 가능하다. let와 const 키워드가 블록레벨 스코프이다. 참고로 코드블럭 내에서 유효한 변수는 지역 변수라고 한다.
실행 컨텍스트 (Execution Context)
실행 컨텍스트(execution context)는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념이다. 쉽게 말하자면 실행 가능한 코드가 실행되기 위해 필요한 환경이다. (코드가 실행된다면 실행 컨텍스트 내부에서 실행되고 있는 것이다.)
이러한 자바스크립트 엔진은 코드를 실행하기 위해서 실행에 필요한 여러 정보를 알고 있어야 한다. 실행에 필요한 여러가지 정보는 변수(전역변수 , 지역변수, 매개변수, 객체의 프로퍼티), 함수 선언, 변수의 유효범위, this가 있다. 위의 정보들을 물리적 객체의 형태로서 자바스크립트 엔진에서 관리한다.
실행 컨텍스트 종류
- Global Execution Context
코드를 실행하며 단 한 개만 정의되는 전역 Context이다. 전역 객체를 생성하며 this 값에 전역 객체를 참조한다. 전역 실행 컨텍스트는 콜 스택에 가장 먼저 추가되며 앱이 종료 될 때 삭제된다. - Functional Execution Context
함수가 실행 될 때 마다 정의되는 컨텍스트이다. 전역 실행 컨텍스트가 단 한 번만 정의되는 것과 달리, 함수 실행 컨텍스트는 매 실행시마다 정의되며 함수 실행이 종료(return)되면 콜 스택에서 제거된다. - Eval Context
eval 함수로 실행한 코드의 컨텍스트로, 보안상 취약한 점이 있어 비권장된다. eval() is evil라는 말이 있을 정도이다.
생성 단계
평가 단계(Creation Phase)
코드 평가 단계: Lexical Environment를 생성하는 단계.
함수와 변수를 기록해둔 Environment Record를 참조. 이러한 Environment Record에서는 선언 정의, 객체 정의가 이뤄지며, this를 바인딩하게 된다. 다만, this는 함수 호출 여부에 따라 참조가 달라진다.
한편으로는 외부 환경을 참조(Outer Environment Reference)하는데, 이때 스코프 체인이 형성된다.
실행 단계(Execution Phase)
코드 실행 단계: 코드가 위에서 아래로 실행되는 단계
변수의 할당 연산자가 존재하면 값이 할당된다. 또한, 함수의 경우 함수 실행 코드가 있을 경우 함수가 실행된다. 새롭게 생성된 컨텍스트가 콜 스택에 쌓이며, 새로운 컨텍스트의 생성 단계와 실행 단계를 거치게 된다.
콜 스택
자바스크립트 엔진은 생성된 컨텍스트를 관리하는 목적(호출된 함수와 순서를 추적)의 콜 스택(Call Stack; 호출 스택)을 갖고 있다. 자바스크립트는 싱글 스레드 형식이기 때문에 런타임에 단 하나의 콜 스택만이 존재하고, 하나의 일만이 처리할 수 있다. 그리고 이러한 콜 스택은 프레임이 쌓이는 Last in First out(LIFO) 형태의 자료구조이다.
자바스크립트 엔진은 전역 범위의 코드를 실행하며 먼저 Global Execution Context를 생성해 스택에 push 한다. 그리고 Global Execution Context가 코드를 실행하는 동안, 함수가 실행 또는 종료되면 Global Execution Context의 위로 Functional Execution Context 스택을 push(추가), pop(제거) 한다.
콜 스택은 최대 스택 사이즈가 정해져있다. 콜 스택에 쌓인 컨텍스트의 스택이 최대치를 넘게 될 경우, ‘RangeError: Maximum call stack size exceeded’라는 에러가 발생한다. 이 에러는 Stack Overflow라고 부르기도 한다.
function callFirst() {
console.log("1번째 함수");
second();
console.log("1번째 함수 한 번 더!");
}
function second() {
console.log("2번째 함수");
third();
console.log("2번째 함수 한 번 더!");
}
function third() {
console.log("3번째 함수");
}
// 초기 호출
callFirst();
- first() 함수가 호출되면 콜 스택에 first가 추가된다.
- first 함수 내에서 console.log("1번째 함수");이 실행되고 출력된다.
- second() 함수가 호출되면 콜 스택에 second가 추가된다.
- second 함수 내에서 console.log("2번째 함수");이 실행되고 출력된다.
- third() 함수가 호출되면 콜 스택에 third가 추가된다.
- third 함수 내에서 console.log("3번째 함수");이 실행되고 출력된다.
- third 함수가 종료되면 콜 스택에서 제거된다.
- second 함수가 종료되면 콜 스택에서 제거된다.
- console.log("2번째 함수 한 번 더!");이 실행되고 출력된다.
- first 함수가 종료되면 콜 스택에서 제거된다.
- console.log("1번째 함수 한 번 더!");이 실행되고 출력된다.
이는 위의 예시를 통해 콜 스택의 실행 순서를 파악할 수 있다. 또한 콜 스택의 함수의 호출 및 종료가 어떻게 관리되는지를 알 수 있다.
클로저 (Closure)
클로저(Closure)는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이라는 것이 공식 문서의 설명이다.
조금 어려운 말이다. 조금 풀어보자면 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)라고 부른다.
즉, 반환된 내부 함수가 자신이 선언됐을 때인 환경(Lexical environment)인 스코프를 기억하여, 자신이 선언되었을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수이다. 이를 더 간단히 말하면 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수다.
클로저에 의해 참조되는 외부 함수의 변수를 자유 변수(Free variable)라고 부른다. 클로저라는 이름은 자유변수에 함수가 닫혀있다(closed)라는 의미로 의역하면 자유변수에 엮여있는 함수라는 뜻이다.
이러한 스코프는 실행 컨텍스트의 관점에서 설명할 수 있다.
내부 함수가 유효한 상태에서 외부 함수가 종료되어 외부 함수의 실행 컨텍스트가 반환되어도, 외부 함수의 실행 컨텍스트 내의 활성 객체(Activation object; 변수, 함수 선언 등의 정보를 가지고 있음)는 내부 함수에 의해 참조되는 한 유효하다. 따라서 내부함수가 스코프 체인을 통해 참조할 수 있는 것을 의미하게 된다.
즉 외부함수가 이미 반환되었어도, 외부함수 내의 변수를 필요로 하는 내부함수가 하나 이상 존재하는 경우 계속 유지된다고 보면 된다. 이때 내부함수가 외부함수에 있는 변수의 복사본이 아니라 실제 변수에 접근한다는 것에 주의하여야 한다.
최종적으로 클로저의 동작 방식을 요약하자면 다음과 같다
- 외부 함수를 실행하면 내부 함수가 생성되며, 내부 함수는 외부 함수의 변수에 접근할 수 있는 클로저를 형성한다.
- 외부 함수의 실행이 끝나더라도 내부 함수가 외부 함수의 변수에 접근할 수 있다.
- 클로저를 통해 외부 함수의 변수는 계속 유지되며, 내부 함수에서 사용될 때마다 해당 변수의 값이 변경된다.
문제가 있다면 변수가 남는 것에서 알 수 있다시피 메모리 누수를 야기할 수도 있다.
그래도 이러한 클로저가 필요한 이유는 많다. 전역 변수를 줄일 수 있고, 비슷한 형태의 코드 재사용률을 높일 수 있고, 상태 유지에도 도움이 된다.
// 데이터 은닉
function createCounter() {
let count = 0; // 외부에서 접근이 어려운 변수 (자유 변수)
return {
increment: function () {
count++;
},
decrement: function () {
count--;
},
getCount: function () {
return count;
},
};
}
let counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
또한, 데이터 은닉(Data Encapsulation)에도 사용할 수 있다. 위처럼 클로저를 사용하면 외부에서 직접적인 접근이 어려운 변수를 가진 객체를 생성하여 데이터를 은닉할 수 있다.
// 커링 1
function curry1(f) {
// 커링 변환을 하는 curry1(f) 함수
return function (a) {
return function (b) {
return f(a, b);
};
};
}
function sum(a, b) {
return a + b;
}
let curriedSum = curry1(sum);
console.log(curriedSum(3)(5)); // 8
// 커링 2
function curry2(x) {
return function (y) {
return x + y;
};
}
console.log(curry2(7)(10)); // 17
이외에도 커링(Currying)이라는 기법이 있다. 이는 여러 개의 인자를 가진 함수를 호출할 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면 누락된 파라미터를 인자로 받는 기법이다.
쉽게 말하자면 fun(a, b, c)처럼 단일 호출로 처리하는 함수를 fun(a)(b)(c)와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 것이다. 함수를 인자로 주면 새로운 함수를 만들어 반환해 준다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>클로저 카운팅</title>
</head>
<body>
<div class="counter-container">
<button id="incrementBtn">1 증가 (++)</button>
<p id="countDisplay">횟수: 0</p>
<button id="decrementBtn">1 감소 (--)</button>
</div>
<script src="script.js"></script>
</body>
</html>
function createCounter() {
let count = 0;
function updateDisplay() {
document.getElementById("countDisplay").innerText = `횟수: ${count}`;
}
function increment() {
count++;
updateDisplay();
}
function decrement() {
count--;
updateDisplay();
}
return {
increment,
decrement,
};
}
let counter = createCounter();
document.getElementById("incrementBtn").addEventListener("click", counter.increment);
document.getElementById("decrementBtn").addEventListener("click", counter.decrement);
마지막으로 상태 유지 예시이다.
createCounter 함수는 클로저를 생성하여 increment와 decrement 함수를 반환한다. 해당 함수들은 외부 변수 count에 접근하여 상태를 유지하고, 각각의 버튼 클릭에 대한 이벤트를 처리한다. 클로저를 통해 count 변수를 유지함으로써 카운터의 상태를 HTML에 계속 업데이트하고 있다.
사족
드디어 프로그래머스 데브코스 11일차가 끝났다. 원래는 오늘 12일차 실습까지 써올리려고 했으나, 아무래도 시간관계 상 그것까지는 힘들 것 같다. 그래도 길고 긴 내용이 끝났으니 이젠 좀 살만하지 않을까 싶다. 정말 다행이다.
당연하지만 본격적으로 다른 것을 해볼 여유도 생길 것 같다. 이런저런 공부하든지 내가 만들고 싶은 것이든지 말이다. 일단 우선적으로 내 수준에서 만들어보고 싶은 건 역시나 노션 클로닝이다. 정확하게는 노션 클로닝을 기반으로 집필용 도구? 비슷한 걸 한 번 만들어보고 싶다. 🤗
'💻 종합 개발 주제 > 📚 웹앱 데브코스' 카테고리의 다른 글
13일차 데브코스 - 클라우딩 어플리케이션 엔지니어링 TIL (0) | 2024.01.18 |
---|---|
12일차 데브코스 - 클라우딩 어플리케이션 엔지니어링 TIL (0) | 2024.01.17 |
11일차 데브코스 pt.2 - 클라우딩 어플리케이션 엔지니어링 TIL (0) | 2024.01.12 |
11일차 데브코스 pt.1 - 클라우딩 어플리케이션 엔지니어링 TIL (2) | 2024.01.11 |
10일차 데브코스 - 클라우딩 어플리케이션 엔지니어링 TIL (4) | 2024.01.09 |