본문 바로가기
Study/JavaScript

You don't know JS yet(1장 4)

by dailycoding777 2025. 2. 20.

4.1 첫번째 기둥 : 스코프와 클로저

함수나 블록 단위로 변수의 스코프(유효 범위)를 한정 짓는 것은 모든 프로그래밍 언어의 근본적인 특징이다.

스코프는 양동이 , 변수는 넣을 구슬

 

스코프 : 변수와 함수가 유효한 범위

즉 , “어떤 변수나 함수에 어디서 접근할 수 있냐?” 하는 범위

스코프 모델이 뭔데?

자바스크립트에서 스코프(Scope) 는 변수에 접근할 수 있는 유효 범위를 의미함. 스코프 모델은 크게 렉시컬(정적) 스코프다이나믹(동적) 스코프로 나뉘는데, 자바스크립트는 렉시컬 스코프(Lexical Scope) 를 따름.

1. 렉시컬 스코프(Lexical Scope, 정적 스코프)

  • 렉시컬(Lexical) - “문법적” | 코드를 작성할 때 결정된다는 의미
  • 스코프 - 범위 (결계)

"변수를 찾을 때, 함수가 선언된 위치를 기준으로 스코프를 결정하는 것” (대부분 프로그래밍 언어는 이러한 방법을 사용)

💡 "변수를 어디에서 선언했냐?" 가 중요함.

코드를 작성할 때 이미 스코프가 결정되며, 실행될 때 바뀌지 않음.

function outer() {
  let a = 10;

  function inner() {
  let b = 0;
    console.log(a); // 10
  }

  return inner;
}

const fn = outer();
fn(); // 실행 시 outer의 a에 접근 가능 (렉시컬 스코프)

🛠 어떻게 동작하냐?

  1. inner 함수가 outer 내부에서 선언되었으므로, inner는 outer의 변수(a)를 참조 가능함.
  2. fn은 inner 함수를 가리키지만, 실행될 때도 a를 찾을 때 원래 선언된 곳의 스코프를 참조함.
  3. 즉, outer가 실행이 끝나도 inner는 a를 기억하고 있음. (클로저)

한 마디로?

👉 "함수를 선언한 위치 기준으로 스코프가 결정된다!"


2. 동적 스코프(Dynamic Scope)

💡 "함수를 어디서 호출했냐?" 가 중요함.

자바스크립트는 이걸 안 쓰지만, Bash 같은 언어에서 사용됨.

function outer() {
  let a = 10;
  inner();
}

function inner() {
  console.log(a);
}

outer(); // ❌ 에러 발생 (inner 함수는 a를 모름)

여기서 inner는 a를 찾을 수 없음.

만약 동적 스코프를 따르는 언어라면, inner()를 outer() 내부에서 실행했기 때문에 a를 찾을 수 있음.

한 마디로?

⇒ "함수를 호출한 위치 기준으로 스코프가 결정된다!" (하지만 JS는 이거 안 씀)


🚀 결론

  1. 자바스크립트는 렉시컬 스코프(Lexical Scope)를 따른다.
  2. 변수를 찾을 때 함수가 "선언된 위치"를 기준으로 스코프를 탐색한다.
  3. 동적 스코프(Dynamic Scope)는 호출한 위치를 기준으로 하지만, JS는 사용하지 않는다.
  4. 렉시컬 스코프 덕분에 클로저(Closure) 같은 개념이 가능하다.

"자바스크립트는 렉시컬 스코프를 사용하며, 함수가 선언된 위치 기준으로 변수 스코프가 결정된다!"


자바스크립트의 두가지 특징

1. 호이스팅

2. var를 사용해 선언한 변수는 해당 변수를 선언한 블록 위치와 상관 없이

함수 기준으로 스코프가 만들어진다.

⇒ var는 블록 스코프가 아니라 "함수 스코프"를 따른다!

  • var로 선언한 변수는 {} 블록을 무시하고, 함수 {} 내부에서만 유효함.
  • 즉, 블록(예: if문, for문 등) 안에서 선언해도 함수 전체에서 접근 가능!

1️⃣ 일반적인 let, const의 블록 스코프

if (true) {
  let a = 10;
  console.log(a); // 10
}

console.log(a); // ❌ 에러 (블록 밖에서는 a 접근 불가)

✔ let은 블록 스코프를 따르므로 {} 바깥에서 a를 사용할 수 없음.


2️⃣ var는 블록을 무시하고 함수 스코프를 따름

if (true) {
  var b = 20;
  console.log(b); // 20
}

console.log(b); // 20 ❗ (블록을 무시하고 바깥에서도 접근 가능)

✔ var는 {} 블록을 무시하고 함수 스코프를 따름.

✔ 즉, if 안에서 선언했지만, 바깥에서도 b를 사용할 수 있음.


3️⃣ var는 함수 스코프를 따르므로, 함수 내부에서만 유효

function foo() {
  var c = 30;
  console.log(c); // 30
}

foo();
console.log(c); // ❌ 에러 (함수 바깥에서는 c 접근 불가)

var는 함수 {} 내부에서 선언되었기 때문에 함수 바깥에서는 접근 불가.


🎯 결론 (면접 답변용 정리)

  • var는 블록 스코프가 아니라 "함수 스코프"를 따른다.
  • 즉, {} 블록 안에서 선언해도, 함수 전체에서 접근 가능하다.
  • 하지만, 함수 {} 안에서 선언되면 함수 바깥에서는 사용할 수 없다. (전역 변수가 아니면!)

🔥 면접 예상 질문

Q. var, let, const의 스코프 차이를 설명하세요.

답변:

👉 var는 블록 스코프를 무시하고 함수 스코프를 따릅니다.

👉 즉, {} 안에서 선언해도 함수 전체에서 접근 가능하지만, 함수 바깥에서는 사용할 수 없습니다.

👉 반면, let과 const는 블록 스코프를 따르므로 {} 밖에서는 접근할 수 없습니다.


프로토 타입(Prototype)

과거 몇년 간 개발자들은 프로토 타입을 사용해 프로토타입 상속이라 부르는 클래스 디자인 패턴을 구현했다.

그런데 ES6 class 키워드가 등장하면서 JS를 객체 지향/클래스 스타일로 개발하자는 움직임이 심화 되었다.

클래스는 프로토타입이 가진 강력한 힘을 기반으로 하는 하나의 패턴에 불과하다.

하기전에 ! Object.create()는 뭐하는 함수임?

Object.create(proto)는 주어진 객체(proto)를 프로토타입으로 하는 새 객체를 생성하는 함수. 즉, 새로운 객체를 만들되, 기존 객체의 속성을 참조할 수 있도록 연결함

const parent = {
 sayHello: function () {
    console.log("Hello from parent!");
  },
};

const child = Object.create(parent); // parent를 프로토타입으로 하는 객체 생성

child.sayHello(); // "Hello from parent!" (child에는 없지만, parent에서 가져옴)

프로토타입 예시)

const person = {
  greet: function () {
    console.log("Hello!");
  },
};
|
| <- 
|
const user = Object.create(person); // person을 프로토타입으로 하는 user 객체 생성

console.log(user.greet()); // "Hello!" (user에는 greet이 없지만, person에서 상속받음)
  • user 객체에는 greet() 메서드가 없음
  • 하지만 프로토타입 체인(Protoype Chain)을 따라 person 객체에서 greet() 를 찾아 사용함.
  • 즉, user 는 person 의 기능을 상속 받음

프로토타입 체인(Prototype Chain)

객체에서 특정 프로퍼티를 찾을 때 , 현재 객체에 없으면 상위 프로토타입 체인으로 올라가서 찾음.

const animal = {
	makeSound : function (){
		console.log("Some sound...")
	},
};

const dog = Object.create(animal);

dog.bark = function(){
	console.log("Woof!")
}

dog.bark(); // "Woof!" (dog에 존재)
dog.makeSound(); // "Some sound..." (dog에는 없지만, prototype에서 가져옴)
  • dog 는 bark 의 메서드는 직접 가지고 있음
  • 하지만 makesound 는 없어서 animal 의 프로토타입에서 찾음

프로토타입을 이용한 함수 확장

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function () {
  console.log(`Hello, my name is ${this.name}`);
};

const user1 = new Person("Alice");
user1.sayHello(); // "Hello, my name is Alice"
  • 생성자 함수(Person)는 prototype 속성을 가지며, 여기에 메서드를 추가하면 모든 인스턴스에서 공유 됨
  • user1.sayHello() 는 user1 객체에 없지만 , 프로토타입 체인을 따라 Person.prototype 에서 찾음

작동위임 패턴이란?(Behavior Delegation Pattern)

(순수번역 - 동작 , 위임 , 패턴 )

  • "상속을 사용하지 않고, 객체 간 기능을 공유하는 방식"
  • 프로토타입 체인을 활용하여 한 객체에서 다른 객체로 기능을 위임(Delegate)하는 패턴.

👉 즉, 부모-자식 클래스를 만드는 대신, 직접 프로토타입 체인을 이용하여 객체 간 기능을 공유하는 방식!

const animal = {
  makeSound() {
    console.log("Some sound...");
  },

};

const dog = Object.create(animal); // animal을 프로토타입으로 사용 (위임)
dog.bark = function () {
  console.log("Woof!"); 
};

dog.bark(); // "Woof!" (dog에 존재)
dog.makeSound(); // "Some sound..." (dog에는 없지만, animal에 위임됨)

dog는 animal을 프로토타입으로 하여 makeSound()를 위임받음.

클래스 없이, 객체 간 직접 위임을 통해 코드 재사용

"상속보다는 위임" → 단순한 객체지향 스타일

🎯 한 줄 정리

1️⃣ 프로토타입(Prototype): 객체가 다른 객체를 원형으로 삼아 속성과 메서드를 상속받는 구조.

2️⃣ 프로토타입 체인(Prototype Chain): 객체에서 속성을 찾을 때, 프로토타입을 따라 상위 객체로 탐색.

3️⃣ 작동 위임 패턴(Behavior Delegation Pattern): 상속 대신, 프로토타입을 활용해 객체 간 기능을 공유하는 패턴.

4️⃣ 결론: 자바스크립트는 프로토타입 기반 언어이며, 클래스 없이도 프로토타입을 이용해 재사용과 기능 위임이 가능하다! 🚀


아니 근데 작동위임패턴이랑 프로토타입이랑 같은 거 아님?

프로토타입 vs 작동 위임 패턴의 차이

  • 프로토타입(Prototype) → 부모(상위 객체)에 기능을 넣고 , 자식(하위객체)이 상속 받음
  • 작동위임 패턴(Behavior Delegation Pattern) → 자식(하위 객체)에서 필요한 기능을 직접 만들고, 부모(프로토타입 객체)에서 필요한 기능을 가져다 쓴다.

🔥 차이 정리

프로토타입 (Prototype-based Inheritance) 작동 위임 패턴 (Behavior Delegation Pattern)

구조 부모(프로토타입)에 기능을 정의하고, 자식이 상속받음 자식 객체를 만들고, 부모 객체(프로토타입)에서 필요한 기능만 위임
사용 방법 prototype을 이용해 메서드를 추가함 Object.create()를 이용해 직접 부모 객체를 연결
예제 Dog.prototype = Object.create(Animal.prototype) const dog = Object.create(animal)

1. 프로토타입 방식 (부모 → 자식)

function Animal(name , color) {
  this.name = name; // 변수
	this.color = color
	makeSound(){ } //
}
// 객체 뽑아주는 기계
Animal.prototype.makeSound = function () {
  console.log(`${this.name} makes a sound`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

// 프로토타입 상속
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const dog1 = new Dog("Buddy", "Golden Retriever");
dog1.makeSound(); // "Buddy makes a sound"

✔ Dog.prototype = Object.create(Animal.prototype); → Dog이 Animal을 상속받음.

✔ makeSound()는 Dog가 직접 가진 것이 아니라, 부모인 Animal에서 상속받음.

즉, 부모(Animal)에 기능을 넣고, 자식(Dog)이 가져가는 방식!

2. 작동 위임 패턴 (자식이 필요할 때 부모에게 요청)

const animal = {
  makeSound() {
    console.log("Some sound...");
  },
};

const dog = Object.create(animal); // animal을 부모 객체로 설정
dog.bark = function () {
  console.log("Woof!");
};

dog.bark(); // "Woof!" (dog에서 직접 선언한 메서드)
dog.makeSound(); // "Some sound..." (animal의 기능을 위임받음)

✔ Object.create(animal)을 사용하면 dog는 animal의 기능을 가져다 씀.

자식(dog)에 기능을 만들고, 부모(animal)의 기능이 필요할 때만 위임받아서 사용

✔ 클래스 없이 객체 간 유연한 관계를 만들 수 있음.

✔ 즉, 부모에서 미리 기능을 정의하는 게 아니라, 필요할 때 위임받아서 사용


타입과 타입 강제 변환

JS에서 타입이 어떻게 작동하는지 잘 모르는 채로 개발하곤 한다.

요즘은 타입스크립트 같은 정적타입 방식 개발로 쏠리고 있는 추세다.

필자 왈 :

JS 타입 매커니즘의 단점 때문에 해결책을 언어 밖에서만 찾을 수 없다는 결론에 동의할 수 없다. ⇒ 타입스크립트 안쓰고 해결 할 수 있다고 생각한다.

  • "use strict"로 위험한 동작 방지
  • 함수 내부에서 런타임 타입 체크
  • Object.freeze()로 불변성 유지
  • Symbol과 Map을 활용해 안전한 데이터 관리
  • typeof, instanceof, isNaN을 활용해 타입 검증

결론은 팀 구성원 모두가 모여 함께 코드를 분석하고 토론하며 근거를 기준으로 결정을 내려야 한다.


학습 순서

  • 2권에 스코프와 클로저 를 제대로 학습해야 함.
    • 렉시컬 스코프,렉시컬 스코프와 클로저의 관계 , 모듈 패턴을 사용해 코드를 체계화 하는 방법을 설명
  • 3권에 객체와 클래스 에서는 JS를 지탱하는 두 기둥을 학습한다.
    • this가 어떻게 작동하는지
    • 객체 프로토타입이 위임을 지원하는 방법과 프로토타입 기반인 class를 사용해 객체지향방식으로 코드를 체계화 하는 방법
  • 4권에 타입과 문법 에서는 3번째 기둥인 타입강제 변환을 설명한다.
  • 잘 복습하고 5권 동기와 비동기 에서는 프로그램 내에서 상태가 변할 때 동기,비동기 방식으로 흐름을 제어하는 방법을 다룬다.
  • 마지막 6권은 JS의 가까운 미래를 점쳐보자. (이건 생략하자)