Loading the catalog…
Loading the catalog…
개발자를 위한 넓고 얕은 컴퓨터 지식, 넓얕컴지 JavaScript는 문법만 보면 시작하기 어렵지 않다. 하지만 왜 선언 전에 변수가 보이지? , 왜 객체를 복사했는데 원본도 바뀌지? , 함수 실행이 끝났는데 변수는 왜 살아 있지? , this는 왜 호출할 때마다 달라지지? 같은 질문에 답하려면 문법보다 언어의 실행 원리를 이해해야 한다. JavaScript의 핵심 개념은 서로 독립되어 있지 않다. 값과 데이터 타입을 이해하면 불변성과 참조 공유가 연결되고, 변수 선언을 따라가면 호이스팅(Hoisting)과 일시적 사각지대(Temporal Dead Zone)가 나온다. 여기에 함수 호출을 더하면 실행 컨텍스트(Execution Context)와 호출 스택(Call Stack)이 등장하고, 스코프(Scope)를 따라가면 렉시컬 환경(Lexical Environment)과 클로저(Closure)가 연결된다. 마지막으로 함수가 어떻게 호출되는지를 보면 this 의 동작까지 이해할 수 있다. 이번 글에서는 값 → 데이터 타입 → 메모리와 복사 → 타입 변환 → 변수 선언 → 함수 → 스코프 → 실행 컨텍스트 → 렉시컬 환경 → 클로저 → this 순서로 JavaScript의 실행 원리를 연결한다. 프로토타입, 클래스, 배열, 반복자(Iterator) 같은 객체 모델은 JavaScript2에서, Promise와 이벤트 루프, Node.js Runtime은 JavaScript3에서 이어서 다룬다. 1️⃣ 값과 코드의 기본 단위 1-1. 값(Value)과 리터럴(Literal) ⭐️ 값(Value)은 프로그램이 처리할 수 있는 실제 데이터다. 10 , "hello" , true , null , { name: "Esther" } 모두 값이다. 리터럴(Literal)은 값을 코드에 직접 표현하는 방법이다. 10 은 숫자 리터럴, "hello" 는 문자열 리터럴, [1, 2, 3] 은 배열 리터럴, { name: "Esther" } 는 객체 리터럴이다. 변수는 값 자체가 아니라 값을 다시 사용할 수 있도록 이름을 붙여 접근하는 수단 이라고 생각하면 이후 실행 컨텍스트와 스코프를 이해하기 쉬워진다. 1-2. 표현식(Expression)과 문(Statement) ⭐️⭐️ 표현식(Expression)은 평가(Evaluation)했을 때 하나의 값이 되는 코드다. 1 + 2 , user.name , getUser() , x > 10 등이 표현식이다. 문(Statement)은 프로그램이 수행할 하나의 명령 단위다. 변수 선언문, 조건문, 반복문, return 등이 대표적이다. const total = price * count; price * count 는 값을 만들어내는 표현식이고, 전체 코드는 변수를 선언하는 문이다. 이 구분이 중요한 이유는 JavaScript에서 값을 요구하는 위치에는 표현식을 넣을 수 있지만 모든 문을 넣을 수 있는 것은 아니기 때문 이다. 1-3. 평가(Evaluation) ⭐️⭐️ JavaScript 엔진은 표현식을 만나면 이를 계산해 하나의 값으로 만든다. 1 + 2 는 평가 후 3 이 되고 user.name 은 객체에서 프로퍼티를 찾아 해당 값으로 평가된다. 함수 호출 표현식인 getUser() 는 함수를 실행한 뒤 반환값으로 평가된다. 이 평가 라는 개념은 뒤에서 실행 컨텍스트를 다룰 때 다시 등장한다. JavaScript 엔진은 코드 전체를 단순히 텍스트로 한 줄씩 읽는 것이 아니라 코드를 해석하고 실행 가능한 상태를 준비한 뒤 각 표현식을 평가한다. 1-4. 세미콜론 자동 삽입(Automatic Semicolon Insertion) ⭐️ JavaScript는 특정 조건에서 세미콜론을 자동으로 삽입한다. 하지만 필요한 위치를 엔진이 알아서 완벽하게 찾아준다 는 의미는 아니다. 대표적인 예가 return 이다. return { name: "Esther" } 개발자는 객체를 반환한다고 생각할 수 있지만 return; 으로 해석되어 undefined 가 반환될 수 있다. 따라서 JavaScript의 줄바꿈은 단순한 코드 스타일 문제만은 아니다. 파서(Parser)가 코드를 어떻게 하나의 문으로 해석하는지 에도 영향을 줄 수 있다. 2️⃣ JavaScript의 데이터 타입 2-1. 원시 타입(Primitive Type)과 객체 타입(Object Type) ⭐️⭐️⭐️ JavaScript의 값은 크게 원시값과 객체로 나누어 볼 수 있다. 원시 타입은 undefined , null , boolean , number , string , symbol , bigint 의 7가지다. 배열(Array), 함수(Function), Date, Map, Set 등을 포함한 나머지 복합적인 값은 객체(Object)를 기반으로 한다. JavaScript 값(Value) │ ├─ 원시 타입(Primitive Type) │ ├─ undefined │ ├─ null │ ├─ boolean │ ├─ number │ ├─ string │ ├─ symbol │ └─ bigint │ └─ 객체(Object) ├─ 일반 객체 ├─ 배열 ├─ 함수 ├─ Date ├─ Map / Set └─ ... 이 구분은 단순한 타입 목록이 아니다. 값이 변경 가능한가, 다른 변수에 할당하면 어떤 일이 일어나는가, 동등 비교에서 어떻게 동작하는가 와 연결된다. 2-2. 동적 타입(Dynamic Typing) ⭐️⭐️ JavaScript에서 변수 자체가 영구적으로 하나의 타입을 가지는 것은 아니다. 값이 타입을 가진다. let value = 10; value = "hello"; 처음에는 숫자 값을 가리키고 이후에는 문자열 값을 가리킨다. 따라서 value는 number 타입 변수다 보다 현재 value가 number 값을 가지고 있다 는 식으로 이해하는 편이 JavaScript의 실제 동작에 가깝다. 이 유연성은 빠른 개발에는 도움이 되지만 대규모 프로그램에서는 실행 전 타입 오류를 발견하기 어려워질 수 있다. TypeScript가 JavaScript 생태계에서 중요한 위치를 차지하는 이유도 여기에서 시작한다. 2-3. typeof를 그대로 믿으면 안 되는 이유 ⭐️⭐️ typeof 는 타입을 간단히 확인할 수 있지만 모든 값을 정확하게 분류하는 기능은 아니다. typeof 10 // "number" typeof "hello" // "string" typeof undefined // "undefined" typeof function(){} // "function" typeof null // "object" typeof [] // "object" 특히 typeof null === "object" 는 JavaScript 초창기부터 남아 있는 특성이다. 배열 역시 객체이므로 typeof [] 는 "object" 다. 배열인지 확인하고 싶다면 Array.isArray() 처럼 목적에 맞는 방법을 사용해야 한다. typeof 는 JavaScript 내부 타입 체계를 완전히 보여주는 함수 가 아니라 특정 범주의 값을 구분하기 위한 연산자 로 이해하는 편이 좋다. 2-4. 숫자(Number)는 생각보다 단순하지 않다 ⭐️⭐️ JavaScript는 일반적인 정수와 실수를 모두 주로 number 타입으로 다룬다. 내부적으로 IEEE-754 배정밀도 부동소수점 형식을 사용하기 때문에 다음과 같은 특성이 나타난다. NaN , Infinity , -Infinity , -0 같은 특수 값이 존재하며 typeof NaN 도 "number" 다. 0.1 + 0.2 === 0.3 // false 십진수로는 정확해 보이는 값도 이진 부동소수점으로 표현하면서 오차가 생길 수 있다. 또한 안전하게 정수를 표현할 수 있는 범위에 한계가 있다. Number.MAX_SAFE_INTEGER 이를 넘어서는 큰 정수를 정확하게 다뤄야 한다면 BigInt 를 사용할 수 있다. 금액, 식별자, Timestamp처럼 정확성 자체가 데이터의 의미인 값 을 다룰 때 JavaScript 숫자 모델을 이해해야 하는 이유다. 2-5. undefined와 null ⭐️⭐️⭐️ 둘 모두 값이 없음 과 관련 있지만 일반적인 사용 의미는 다르다. undefined 는 아직 값이 할당되지 않았거나 객체에 해당 프로퍼티가 존재하지 않을 때처럼 JavaScript 실행 과정에서 자연스럽게 등장하는 경우가 많다. null 은 개발자가 의도적으로 값이 존재하지 않음 을 표현할 때 사용한다. let value; console.log(value); // undefined const user = { profile: null }; 실제 API에서는 필드 자체가 제공되지 않았다 와 필드의 값이 명시적으로 비어 있다 가 다른 의미일 수도 있다. 따라서 둘을 무조건 같은 빈 값으로 처리하면 데이터 모델의 의미를 잃을 수 있다. 2-6. 문자열(String)은 원시값인데 어떻게 메서드를 사용할까? 문자열은 원시값이다. 그런데 다음 코드가 가능하다. "hello".toUpperCase() 원시값 자체가 일반 객체로 영구적으로 바뀌는 것은 아니다. JavaScript는 필요한 순간에 String 같은 내장 객체의 기능을 이용할 수 있도록 처리한다. 이러한 동작을 이해하면 원시값(Primitive)과 String , Number , Boolean 같은 래퍼 객체(Wrapper Object)를 혼동하지 않게 된다. 명시적으로 new String("hello") 같은 객체를 만드는 것과 "hello" 라는 원시 문자열은 서로 다른 값이다. 3️⃣ 변수와 메모리를 어떻게 생각해야 할까 3-1. 변수와 식별자(Identifier) ⭐️⭐️ 변수는 값을 저장하고 다시 사용하기 위한 수단이며 식별자(Identifier)는 변수, 함수, 클래스 등의 대상을 구분하는 이름이다. const user = { name: "Esther" }; user 라는 식별자를 이용해 객체에 접근한다. 학습할 때 흔히 변수 하나를 메모리 상자 하나로 그리지만 실제 V8 같은 엔진은 훨씬 복잡한 메모리 최적화를 사용한다. 따라서 메모리 그림은 실제 구현 그대로가 아니라 이름과 값의 연결 관계를 설명하기 위한 개념 모델 로 사용하는 것이 좋다. 3-2. 원시값의 불변성(Immutability) ⭐️⭐️⭐️ 원시값은 불변(Immutable)이다. let str = "hello"; str = "world"; 여기서 "hello" 라는 문자열 자체를 "world" 로 수정한 것이 아니다. str 이 새로운 문자열 값을 가리키도록 재할당한 것이다. 중요한 차이는 다음과 같다. 변수는 변경될 수 있다. 원시값 자체는 변경되지 않는다. 문자열의 특정 위치를 수정할 수 없는 것도 같은 이유다. let str = "hello"; str[0] = "H"; console.log(str); // "hello" 3-3. 객체의 변경 가능성(Mutability) ⭐️⭐️⭐️ 객체는 생성된 이후 내부 프로퍼티를 변경할 수 있다. const user = { name: "Esther" }; user.name = "Reina"; const 인데도 변경 가능한 이유는 const 가 객체 자체를 불변으로 만드는 기능이 아니기 때문이다. 다음은 불가능하다. user = {}; 변수 user 가 다른 값을 다시 가리키게 만드는 재할당은 금지된다. 하지만 다음은 가능하다. user.name = "Reina"; user 가 가리키는 객체는 그대로이고 그 객체 내부의 프로퍼티만 변경했기 때문이다. 4️⃣ 값 복사와 참조 공유 4-1. 원시값 복사 ⭐️⭐️⭐️ let a = 10; let b = a; b = 20; b 를 변경해도 a 는 여전히 10 이다. 원시값은 값의 의미가 독립적으로 전달되기 때문에 두 변수를 각각 변경할 수 있다. 4-2. 객체 참조 공유 ⭐️⭐️⭐️ 객체에서는 다른 현상이 나타난다. const a = { count: 1 }; const b = a; b.count = 2; console.log(a.count); // 2 두 변수가 같은 객체에 접근하고 있기 때문이다. JavaScript를 설명할 때 객체는 참조에 의해 전달된다(Pass-by-Reference) 라고 표현하기도 하지만 정확하게 이해하려면 조금 더 주의해야 한다. 함수에 전달되는 것 역시 값 이다. 다만 객체를 다루는 변수의 값이 객체에 접근할 수 있는 참조이기 때문에 그 참조 값이 복사되면서 양쪽에서 동일한 객체에 접근할 수 있게 된다. a ─┐ ├──▶ { count: 1 } b ─┘ 4-3. 얕은 복사(Shallow Copy) ⭐️⭐️⭐️ 전개 구문(Spread)을 사용하면 객체를 간단히 복사할 수 있다. const copied = { ...original }; 하지만 바깥 객체만 새로운 객체가 된다. const original = { profile: { age: 20 } }; const copied = { ...original }; copied.profile.age = 30; original.profile 과 copied.profile 은 같은 객체를 가리킨다. 따라서 원본의 profile.age 도 변경된다. 4-4. 깊은 복사(Deep Copy) ⭐️⭐️ 중첩된 객체까지 독립적인 객체로 만들려면 깊은 복사(Deep Copy)가 필요하다. 현대 환경에서는 지원 가능한 값이라면 structuredClone() 을 사용할 수 있다. 과거에는 다음과 같은 방식도 자주 사용됐다. JSON.parse(JSON.stringify(obj)) 하지만 undefined , 함수, BigInt , 일부 내장 객체 등의 데이터를 제대로 보존하지 못할 수 있기 때문에 일반적인 깊은 복사 도구로 보는 것은 위험하다. 복사는 단순한 문법 문제가 아니라 새로운 객체가 필요한가, 같은 객체를 공유해도 되는가 를 결정하는 설계 문제다. 5️⃣ 타입 변환과 값 비교 5-1. 암묵적 타입 변환(Type Coercion) ⭐️⭐️⭐️ JavaScript는 서로 다른 타입이 연산에 사용될 때 자동으로 타입을 변환하기도 한다. "10" + 1 // "101" "10" - 1 // 9 + 에서는 문자열 연결이 가능하므로 숫자가 문자열로 변환된다. 반면 - 는 숫자 연산이므로 문자열이 숫자로 변환된다. 이런 결과를 하나씩 암기하는 것보다 현재 연산자가 어떤 값을 필요로 하고 어떤 변환이 일어나는지 를 따라가는 편이 중요하다. 5-2. 명시적 타입 변환(Type Conversion) ⭐️⭐️ 개발자가 변환 의도를 직접 표현할 수도 있다. Number() , String() , Boolean() 이 대표적이다. 명시적인 변환은 코드를 읽는 사람에게도 의도를 분명히 보여주기 때문에 복잡한 데이터 변환에서는 암묵적 변환에 지나치게 의존하지 않는 편이 좋다. 5-3. 참 같은 값(Truthy)과 거짓 같은 값(Falsy) ⭐️⭐️⭐️ JavaScript의 조건식에는 반드시 true 와 false 만 들어갈 필요가 없다. 값이 불리언(Boolean)으로 변환되었을 때 거짓으로 평가되는 대표적인 값은 다음과 같다. false , 0 , -0 , 0n , "" , null , undefined , NaN 나머지 대부분의 값은 Truthy다. 특히 빈 배열 [] 과 빈 객체 {} 도 Truthy다. if ([]) { // 실행됨 } 비어 있으니까 false겠지 라는 직관과 다르게 동작할 수 있기 때문에 자주 실수한다. 5-4. 느슨한 동등 비교(==)와 엄격한 동등 비교(===) ⭐️⭐️⭐️ == 는 타입이 다르면 암묵적 타입 변환 후 비교할 수 있다. 0 == false // true === 는 타입을 변환하지 않고 타입과 값을 함께 비교한다. 0 === false // false 일반적인 애플리케이션에서는 === 를 기본으로 사용하는 편이 예측하기 쉽다. 하지만 === 도 모든 값을 완벽하게 구분하지는 않는다. NaN === NaN // false 0 === -0 // true 이런 특수한 차이까지 구분해야 한다면 Object.is() 를 사용할 수 있다. 5-5. 논리 연산자의 단축 평가(Short-circuit Evaluation) ⭐️⭐️ && , || 는 반드시 Boolean 값만 반환하지 않는다. || 는 왼쪽 값이 Falsy라면 오른쪽 값을 반환한다. 0 || 10 // 10 문제는 0 , "" , false 도 정상적인 데이터일 수 있다는 것이다. 널 병합 연산자(Nullish Coalescing) ?? 는 null 또는 undefined 일 때만 오른쪽 값을 사용한다. 0 ?? 10 // 0 따라서 기본값을 처리할 때 두 연산자를 같은 의미로 사용하면 안 된다. 5-6. 선택적 연결(Optional Chaining) ⭐️⭐️ 선택적 연결 ?. 은 접근 중간의 값이 null 또는 undefined 라면 오류를 발생시키지 않고 undefined 를 반환한다. user.profile?.address?.city 중첩 객체를 안전하게 읽을 때 유용하지만 데이터가 존재해야 하는데도 ?. 를 남용하면 실제 오류를 조용히 숨길 수도 있다. 6️⃣ var, let, const 6-1. var ⭐️⭐️⭐️ var 는 함수 레벨 스코프(Function-level Scope)를 가진다. if (true) { var value = 10; } console.log(value); // 10 블록 {} 을 벗어나도 같은 함수 안이라면 접근할 수 있다. 또한 같은 스코프에서 중복 선언이 가능하다. var value = 10; var value = 20; 큰 코드베이스에서는 이런 느슨한 규칙이 실수로 이어질 수 있다. 6-2. let ⭐️⭐️⭐️ let 은 블록 레벨 스코프(Block-level Scope)를 가진다. if (true) { let value = 10; } console.log(value); // ReferenceError 같은 스코프에서 중복 선언은 허용되지 않지만 재할당은 가능하다. 6-3. const ⭐️⭐️⭐️ const 역시 블록 스코프를 가지며 중복 선언할 수 없다. 또한 반드시 선언과 동시에 초기화해야 하며 이후 재할당할 수 없다. const count = 10; // count = 20; // 불가능 다만 앞에서 보았듯 객체 내부 프로퍼티 변경까지 막는 것은 아니다. 6-4. 현대 JavaScript에서 무엇을 기본으로 사용할까? ⭐️⭐️ 일반적으로는 const 를 기본으로 사용하고 값 자체를 다시 할당해야 할 때 let 을 사용하는 방식이 읽기 쉽다. var 를 무조건 사용하면 안 된다는 규칙이라기보다 블록 스코프와 중복 선언 제한을 제공하는 let/const가 코드의 상태 변경 범위를 더 명확하게 보여준다 는 것이 핵심이다. 7️⃣ 호이스팅(Hoisting) 7-1. 호이스팅은 코드를 위로 끌어올리는 기능이 아니다 ⭐️⭐️⭐️ 흔히 다음 코드를 설명할 때 var 선언이 코드 위로 올라갔다 고 말한다. console.log(value); // undefined var value = 10; 하지만 실제 소스코드가 물리적으로 이동하는 것은 아니다. JavaScript 엔진이 코드를 실행하기 전에 현재 실행 영역에 어떤 선언들이 존재하는지 먼저 처리하기 때문에 선언문보다 앞에서도 식별자의 존재를 알 수 있는 것이다. 이 동작을 이해하려면 평가 단계 와 실행 단계 를 구분해야 한다. 7-2. 변수 선언은 한 번에 일어나지 않는다 ⭐️⭐️⭐️ 개념적으로 변수 선언은 다음 과정으로 나누어 생각할 수 있다. 선언 → 초기화 → 할당 선언은 식별자의 존재를 등록하고, 초기화는 값을 저장할 수 있는 상태를 준비하며, 할당은 실제 값을 연결한다. var 는 선언과 초기화 과정에서 undefined 가 연결되는 반면 let/const 는 초기화되기 전에 접근할 수 없다. 이 차이가 호이스팅 결과의 차이를 만든다. 7-3. 일시적 사각지대(Temporal Dead Zone) ⭐️⭐️⭐️ console.log(value); const value = 10; value 라는 식별자가 아예 존재하지 않는 것은 아니다. 현재 스코프에 이미 등록되어 있지만 선언문이 실행되어 초기화되기 전까지 접근할 수 없다. 이 구간을 일시적 사각지대(TDZ)라고 한다. 따라서 let과 const는 호이스팅되지 않는다 는 설명은 부정확하다. 호이스팅은 되지만 초기화 이전 접근이 차단된다. 8️⃣ 함수는 JavaScript에서 특별한 값이다 8-1. 일급 객체(First-
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
[넓얕컴지 #5] JavaScript 1 실행 원리: 데이터 타입부터 실행 컨텍스트, 클로저, this까지. 개발자를 위한 넓고 얕은 컴퓨터 지식, 넓얕컴지 JavaScript는 문법만 보면 시작하기 어렵지 않다. 하지만 왜 선언 전에 변수가 보이지? , 왜 객체를 복사했는데 원본도 바뀌지? , 함수 실행이 끝났는데 변수는 왜 살아 있지? , this는 왜 호출할 때마다 달라지지? 같은 질문에 답하려면 문법보다 언어의 실행 원리를 이해해야 한다. JavaScript의 핵심 개념은 서로 독립되어 있지 않다. 값과 데이터 타입을 이해하면 불변성과 참조 공유가 연결되고, 변수 선언을 따라가면 호이스팅(Hoisting)과 일시적 사각지대(Temporal Dead Zone)가 나온다. 여기에 함수 호출을 더하면…
Open source