Chapter 1 스코프
시작 전에 내가 궁금했던 건 보라색으로 칠하고 밑에 답을 달았다.
js엔진이 어떻게 변수를 조직하고 관리하는지 자세히 알지 못한채로 코드를 작성한다.
그래서 우리가 알아야 할 것은
- 주어진 구문에서 접근 가능한 변수를 js엔진이 어떻게 결정하는지?
- 이름이 같은 두 변수가 있을 때 어떻게 처리하는지?
이 두가지를 잘 알아야하는데 이 질문들의 답은 스코프에 있다.(2.2장에서 다룰거임)
1.1 책에 대하여
2부에선 스코프 시스템 , 클로저 , 모듈 디자인 패턴의 강력함에 대해 알아본다.
js는 컴파일 언어인지 인터프리터 언어인지는 ECMA에 정의된 바가 없고
스크립트 언어로 분류된다**.**
그런데 실제 JS는 실행 전 별도의 단계에서 파싱 , 컴파일이 일어난다.
js에서 함수는 일급값(first-class-value)기 떄문에 다음과 같이 할 수 있다.
- 숫자나 문자열 처럼 변수에 할당
- 다른 곳으로 넘기기 (함수의 인자로 전달하거나, 함수의 반환값으로 사용할 수 있다)
1.2 컴파일 VS 인터프리트
컴파일레이션이란?
소스 코드(Source Code)를 컴퓨터가 이해할 수 있는 기계어(Machine Code)로 변환하는 과정
소스 코드(사람이 이해할 수 있는 코드) → 컴퓨터가 실행할 수 있는 코드
컴파일레이션 과정 (일반적인 컴파일 과정)
- Lexical Analysis (어휘 분석): 코드에서 키워드, 변수, 연산자 등을 찾아 토큰(Token)으로 변환
- Parsing (구문 분석): 토큰을 해석하여 프로그램 구조를 분석 (문법 체크)
- Semantic Analysis (의미 분석): 문법적으로 맞아도 의미적으로 오류가 없는지 확인
- Optimization (최적화): 실행 속도를 빠르게 하거나 코드 크기를 줄이는 과정
- Code Generation (코드 생성): 최종적으로 기계어로 변환하여 실행 파일 생성
JS는 컴파일을 할까?
JavaScript는 인터프리터 언어로 알려져 있지만, 최신 JS 엔진(예: V8)은 JIT(Just-In-Time) 컴파일을 사용해서 일부 코드를 실행 중에 컴파일해서 속도를 최적화한다.
- 인터프리터 : 코드를 한줄씩 실행한다.(한줄씩 읽고, 해석(interpret)해서 즉시 실행)
JS(V8)에서의 실행 과정
짧게 정리해보면 이렇다
- Lexical Analysis (어휘 분석) → 토큰(Token)으로 쪼갠다.
- Parsing (구문 분석) → AST(Abstract Syntax Tree) 생성
- Bytecode 생성 → AST를 바이트코드로 변환 (Ignition 인터프리터 사용)
- JIT(Just-In-Time) 컴파일링 → 실행 중 프로파일링 후 최적화 (TurboFan)
- 기계어(Native Code) 변환 → 자주 실행되는 코드를 최적화
- 실행
일반적인 컴파일은 AOT(Ahead-Of-Time) 방식이고,
V8의 JS 실행 과정은 JIT(Just-In-Time) 방식이라 다르다.
AOT : 실행 전에 미리 전체 코드를 기계어로 변환하는 방식
JIT :필요한 순간에 딱 맞춰 컴파일 한다.
밑은 이해를 돕기 위한 상세 실행과정이다.
1️⃣ Lexical Analysis (어휘 분석)
문자열을 토큰(Token)으로 쪼갠다.
let x = 10;
let y = x + 5;
console.log(y);
2️⃣ Parsing (구문 분석)
토큰을 추상구문 트리 AST(Abstract Syntax Tree)로 변환한다.
여기서 코드의 구조만 저장하고, 변수의 값은 직접 저장되지 않음!
// AST 생성
VariableDeclaration (let)
├── VariableDeclarator
│ ├── Identifier (x) <-- 변수명만 저장됨!
│ └── Literal (10) <-- 리터럴 값 (10)
VariableDeclaration (let)
├── VariableDeclarator
│ ├── Identifier (y)
│ └── BinaryExpression (+)
│ ├── Identifier (x) <-- 변수명만 저장됨 (값 없음)
│ └── Literal (5) <-- 리터럴 값 (5)
ConsoleCall (console.log)
├── Identifier (y) <-- 변수명만 저장됨 (값 없음)
3️⃣ Bytecode 생성
AST를 바이트코드로 변환 후 실행 (바이트 코드 생성기 : Ignition 인터프리터 사용)
🚨 하지만 변수의 실제 값은 바이트코드 단계에서 메모리에 저장만 되고, 실행할 때 불러와진다.
LDA 10 // x = 10 (메모리에 저장)
STA x // x 저장
LDA x // 실행 시 메모리에서 x 불러오기
ADD 5 // + 5 연산
STA y // y 저장
LOG y // console.log(y)
✔ 여기서 LDA x에서 실제 변수 값이 실행될 때 불러와짐!
4️⃣ JIT(Just-In-Time) 컴파일링
👉 실행 중 프로파일링 후 최적화 (최적화 엔진 : TurboFan)
- 실행할 때 자주 실행되는 코드(Hot Code) 를 감지
- 반복 실행되는 연산을 최적화 & 불필요한 연산 제거
5️⃣ 기계어(Native Code) 변환
👉 자주 실행되는 코드를 기계어로 최적화하고, 자주 안 쓰는 건 바이트코드 상태로 유지한다.
- x + 5 같은 연산이 반복 실행되면, TurboFan이 기계어 변환하여 더 빠르게 실행
6️⃣ 실행 (Execution Context에서 실행됨)
- 바이트코드 & 기계어 코드가 실행됨
- 변수 값이 메모리에서 불러와지고 연산이 수행됨
- console.log() 같은 함수가 호출됨
실행 단계에서 변수 값이 메모리에 불러와지는 과정
✅ 예제 코드
let x = 10;
let y = x + 5;
console.log(y);
💡 이 코드의 실행 흐름을 보면?
1️⃣ 실행 컨텍스트 생성 (Execution Context 생성)
- JS 엔진이 실행할 때, "실행 컨텍스트(Execution Context)"를 만듦
- 실행 컨텍스트는 변수와 함수 정보를 저장하는 메모리 공간
📌 변수가 저장될 곳 (스코프에 따라 다름)
✔ 전역 변수 → Global Execution Context에 저장
✔ 함수 내부 변수 → Function Execution Context에 저장
2️⃣ 변수 값이 메모리에 저장됨
let x = 10;
- x라는 변수를 실행 컨텍스트의 "변수 환경(Variable Environment)"에 저장
- 메모리에 x = 10 할당됨
📌 메모리 상태:
x → 10
3️⃣ 실행 컨텍스트에서 변수 값을 불러와 연산 수행
let y = x + 5;
- JS 엔진이 x의 값을 메모리에서 찾아와서 +5 연산 수행
- 결과를 다시 메모리에 y = 15 로 저장
📌 메모리 상태:
x → 10
y → 15
4️⃣ console.log(y) 실행
console.log(y);
- y 값을 실행 컨텍스트에서 찾아 출력
- 실행 결과: 15
실행의 결론
- 변수 값은 실행 컨텍스트의 변수 환경(Variable Environment)에 저장됨!
- 실행(Execution) 단계에서 JS 엔진이 변수 값을 메모리에서 찾아와 연산 수행!
- 즉, 변수 값은 바이트코드 변환 후 "실행될 때" 메모리에서 불러와 사용됨!
최종 결론은
- 1~5번까지는 "실행 준비" 과정
- 실제 실행(Execution)은 6번째 단계에서 이루어짐
- 변수 값도 실행단계에서 메모리에서 불러와 연산 된다.
AST (추상구문 트리)는 코드의 구조(설계) 아닌가? 문자를 담을 수 있나?
AST : 소스 코드의 구조를 tree 형태로 표현한 것
- js 엔진이 코드를 이해하고 분석하는 데이터 구조
- 변수명,연산자,함수 호출 정보 등을 포함
- 값 자체는 없지만, “리터럴 값”이 포함됨
- 리터럴 값 : 그 자체로 값을 가지는 데이터
- 코드에서 직접 쓰인 값
- 변수명이나 연산 결과가 아니라 , 코드에 직접 적힌 값 (숫자, 문자열 등)
📌 해석VariableDeclaration (let) ├── VariableDeclarator │ ├── Identifier (x) │ └── Literal (10) <-- 리터럴 값 VariableDeclaration (let) ├── VariableDeclarator │ ├── Identifier (y) │ └── BinaryExpression (+) │ ├── Identifier (x) <-- 변수 (리터럴 아님!) │ └── Literal (5) <-- 리터럴 값
- 10, 5 → 리터럴 값 (Literal)
- x, y → 변수 (Identifier)
- x + 5 → x는 변수이므로 AST에 값 없이 이름만 저장됨
- 반면, "5"는 소스 코드에서 직접 사용된 값이므로 리터럴 값으로 저장됨
- 🔹 AST 구조
- let x = 10; let y = x + 5;
- 리터럴 값 : 그 자체로 값을 가지는 데이터
그러니까 AST는 다 넣을 수 있다.
- 변수(Variable) 정보 → 변수명(Identifier) & 할당된 값(Literal)
- 리터럴(Literal) 값 → 숫자, 문자열, 불리언 등
- 연산자(Operator) 정보 → +, -, *, /, =, === 등
- 함수(Function) 정보 → 함수 선언, 파라미터, 반환값 등
- 제어문(Control Flow) → if, for, while, switch 같은 흐름 제어
- 객체(Object), 배열(Array) → {}, [] 같은 구조체
프로파일링(Profiliing) : 실행 중 코드패턴을 분석하는 과정
- 어떤 함수가 자주 실행되는지,변수가 어떻게 쓰이는지 등을 감지
비슷한 개념과 차이점
개념 설명
컴파일 (Compilation) | 코드 전체를 미리 번역 후 실행 (C, C++ 등) |
인터프리트 (Interpretation) | 코드를 한 줄씩 읽고 실행 (Python, JS) |
JIT 컴파일 | 실행 중에 필요한 부분만 즉시 번역 (JS 최신 엔진) |
⇒컴파일레이션은 코드 변환 과정이며, JS도 부분적으로 컴파일을 한다.
1.4 책에서 말한 타깃과 소스
선언을 제외하고 프로그램 내 모든 변수와 식별자는 할당의 타깃이나 값의 소스, 둘중 하나 역할을 합니다.
⇒ 모든 변수와 식별자는 "값을 받거나(타깃)" 혹은 "값을 제공하는 역할을 한다(값의 소스)”
식별자란? - 이름이 있는 모든 요소 (변수,함수,클래스,객체)
1.5 런타임에 스코프 변경하기
스코프는 프로그램이 컴파일 될 때 결정되고 , 런타임 환경에는 영향을 받지 않는다.
비엄격 모드에서는 런타임에도 프로그램의 스코프를 수정할 수 있는 두가지 방법이 있긴하다. (쓰면 안된다.)
- eval() 함수
- with문
1. eval() 함수
eval()은 문자열을 코드처럼 실행하는 함수
하지만 보안에 취약하고 최적화도 망가뜨려서 절대 쓰면 안 됨
🔹 eval() 예제 (절대 쓰면 안 됨)
let x = 10;
eval("x = 20; console.log(x);"); // 20 (x의 값이 바뀜)
console.log(x); // 20 (스코프가 강제로 변경됨)
- 원래 x가 10이었는데, eval()을 쓰면 문자열을 실행해서 런타임 중에 변수의 값이 바뀜.
- 보안 문제: 외부에서 eval()을 통해 악성 코드 실행 가능. (XSS 공격에 취약)
- 최적화 망침: 자바스크립트 엔진이 eval() 때문에 코드 최적화를 할 수 없음.
2. with 문
객체 속성을 마치 지역 변수처럼 접근할 수 있게 만드는 문법
하지만 스코프를 예측하기 어렵고, 엄격 모드에서는 아예 금지됨
🔹 with 예제 (절대 쓰면 안 됨)
let obj = { a: 10, b: 20 };
with (obj) {
console.log(a); // 10 (obj.a를 그냥 a처럼 씀)
console.log(b); // 20 (obj.b도 그냥 b처럼 씀)
}
- with (obj)를 쓰면 obj의 속성을 마치 로컬 변수처럼 접근할 수 있음.
- 문제점: 어떤 변수를 참조하는지 헷갈림.
- obj.a인지, 바깥의 a인지 헷갈려서 코드 유지보수가 어려움.
- 그래서 엄격 모드("use strict";)에서는 아예 금지됨.
- let a = 100; let obj = { a: 10 }; with (obj) { console.log(a); // 10? 100? (헷갈림!) }
결론: eval()과 with는 절대 쓰지 말아야 한다.
✔ eval() → 보안 위험 + 최적화 파괴
✔ with → 스코프 혼란 + 엄격 모드에서는 금지됨
엄격 모드("use strict";) vs 비엄격 모드
엄격 모드는 자바스크립트의 위험한 기능을 막고, 코드의 안정성을 높이는 기능이다.
엄격 모드의 주요 차이점
구분 비엄격 모드 (기본) 엄격 모드 ("use strict";)
1. 선언 없이 변수 할당 | O (전역 변수로 생성) | ❌ 오류 발생 |
2. this의 값 (window 바인딩) | O (undefined가 아니라 window) | ❌ undefined |
3. 중복된 매개변수 허용 | O 가능 | ❌ 오류 발생 |
4. eval()과 arguments 조작 가능 | O 가능 | ❌ 조작 금지 |
5. with 사용 가능 | O 가능 | ❌ 사용 금지 |
엄격모드 차이점 예제
1. 선언 없이 변수 할당 (비엄격 모드에서는 가능)
// 비엄격 모드
x = 10; // 전역 변수로 자동 생성됨 (위험!)
console.log(x); // 10
// 엄격 모드
"use strict";
x = 10; // ❌ 오류 발생! (ReferenceError: x is not defined)
✔ 엄격 모드는 선언 없이 변수를 만들 수 없게 막음 → 실수 방지!
2. this의 값 (전역 스코프에서 다름)
// 비엄격 모드
console.log(this); // Window 객체 출력
// 엄격 모드
"use strict";
console.log(this); // ❌ undefined (더 안전함)
✔ 비엄격 모드에서는 this가 window를 가리키지만, 엄격 모드에서는 undefined가 됨!
✔ this 바인딩 실수를 막아줌
3️⃣ 중복된 매개변수 허용 여부
// 비엄격 모드
function add(a, a, c) {
return a + c; // 문제 있지만 실행됨
}
console.log(add(1, 2, 3)); // 5 (중복된 `a` 때문에 헷갈림)
// 엄격 모드
"use strict";
function add(a, a, c) {
return a + c;
}
// ❌ SyntaxError: Duplicate parameter name not allowed in this context
✔ 매개변수 이름 중복을 방지해서 코드 실수를 줄여줌! ✅
4️⃣ eval()과 arguments 조작 금지
"use strict";
eval("var x = 10;");
console.log(x); // ❌ ReferenceError: x is not defined
✔ eval()이 전역 변수를 만들지 못하게 제한됨 → 보안 강화
✔ arguments 객체도 조작 불가
5️⃣ with 문 사용 금지
// 비엄격 모드
with (Math) {
console.log(PI); // 3.141592653589793 (가능)
}
// 엄격 모드
"use strict";
with (Math) {
console.log(PI);
}
// ❌ SyntaxError: Strict mode code may not include a with statement
✔ with 문이 스코프를 헷갈리게 만드니까 아예 금지다.
1.6 렉시컬 스코프에 대한내용인데 챕터2에 제대로 다루니 바로 넘어간다.
2. 렉시컬 스코프 (이건 스코프의 대한 규칙이에요.)
1. 스코프: 함수나 변수에 접근 가능한 유효 범위. 렉시컬 스코프(정적)스코프 규칙을 따름
2. 렉시컬 스코프(Lexical Scope) 는 "코드가 작성된 위치"에 따라 스코프가 결정되는 것
특정 변수나 함수를 어느 위치에서 참조할 수 있는지를 나타내며
만약 스코프에 벗어난 영역에서 참조한 영역에서 참조할 경우 Reference Error가 발생한다.

이들 모두 렉시컬 스코프(정적 스코프)의 규칙에 따른다.

스코프 체인
변수를 찾을 때, 현재 스코프에서 없으면 바깥(상위) 스코프로 계속 올라간다.
- JS는 렉시컬 스코프(선언된 위치 기준)을 따르니까 함수 안에서 변수를 찾을 때 바깥(부모)스코프를 따라가며 찾는 과정이다.
- 렉시컬 스코프 덕분에, 함수가 선언된 위치 기준으로 스코프 체인이 결정됨
엔진이 모든 렉시컬 스코프를 뒤졌는데도 못찾으면
⇒ Reference Error가 발생한다.
let globalVar = "나는 전역 변수";
function outer() {
let outerVar = "나는 outer 변수";
function inner() {
let innerVar = "나는 inner 변수!";
console.log(innerVar); // "나는 inner 변수" (자기 스코프에서 찾음)
console.log(outerVar); // "나는 outer 변수" (부모 스코프에서 찾음)
console.log(globalVar); // "나는 전역 변수" (최상위 스코프에서 찾음)
}
inner();
}
outer();
스코프체인의 구조를 트리처럼 보면 이래요
Global Scope
├─ outer() Scope
│ ├─ inner() Scope
- 안쪽함수(inner)는 바깥함수(outer)와 전역 스코프를 볼 수 있음
- 바깥함수(outer)는 안쪽함수(inner)를 볼 수 없다.
중첩 스코프(Nested Scope) : 말 그대로 스코프가 여러 겹 쌓여 있는 것
Not declared와 Undeclared의 차이
개념 설명 예제 결과
Not Declared | 변수가 아예 선언되지 않음 | console.log(x); | ❌ ReferenceError 발생 |
Undeclared | 변수 선언 없이 값이 할당됨 (비엄격 모드에서 가능) | x = 10; console.log(x); | ✅ 정상 실행됨 (전역 변수로 생성됨) |
함수실행 될 때 콜스택이 생성 되는데 콜스택 안엔 뭐가 들어있고 그안엔 뭐가 들어있음?


자바스크립트 스코프를 나눠보면요
- 전역 스코프 (할아버지)
- 로컬 스코프 (함수 스코프 , (코드)블록 스코프)
전역 스코프 안에,로컬 스코프가 있는 거고
로컬스코프의 종류는 함수 스코프, 블록 스코프라는 게 있다.
var vs let vs const의 스코프 차이
선언 키워드 따르는 스코프 특징
var | 함수 스코프 | 블록 {}을 무시하고 함수 안에서만 제한됨 |
let | 블록 스코프 | 블록 {} 내부에서만 유효 |
const | 블록 스코프 | 블록 {} 내부에서만 유효, 재할당 불가 |
let,const와 var의 차이는 나중에 한번 더 다뤄보겠다.
'Study > JavaScript' 카테고리의 다른 글
You don't know JS yet(1장 4) (1) | 2025.02.20 |
---|---|
You don't know JS yet(3-2~4) 클로저,this,prototype (0) | 2025.02.13 |
You don't know JS yet(3-1) 이터레이션 (0) | 2025.02.13 |
You don't know JS yet(2) (0) | 2025.02.06 |
You don't know JS yet(1.5~1.8) (0) | 2025.01.23 |