Skip to Content
BlogJavaScript 정리기초 정리함수 심화 스코프, 클로저, this 바인딩

함수 심화 스코프, 클로저, this 바인딩

스코프 (함수 스코프, 블록 스코프, 스코프 체인)

스코프는 변수에 접근할 수 있는 범위를 뜻한다. JS에는 크게 두 종류의 스코프가 있다.

함수 스코프

var로 선언한 변수가 갖는 스코프다. 함수 단위로만 범위가 구분되고, if나 for 같은 블록은 스코프 경계가 되지 않는다.

function example() { if (true) { var x = 10; } console.log(x); // 10 (블록을 벗어나도 접근 가능) } example();

블록 스코프

let, const로 선언한 변수가 갖는 스코프다. {}로 감싸인 블록 단위로 범위가 구분된다.

function example() { if (true) { let y = 10; } console.log(y); // ReferenceError: y is not defined }

이 차이 때문에 let, const가 var보다 더 예측 가능한 코드를 만들 수 있어서 요즘은 기본적으로 let, const를 사용한다.

스코프 체인

함수는 자신이 선언된 위치를 기준으로 상위 스코프를 참조할 수 있는 연결고리를 가진다. 이를 스코프 체인이라고 한다. 변수를 찾을 때 현재 스코프에 없으면 바깥쪽 스코프로 계속 거슬러 올라가며 찾는다.

const outer = "바깥"; function first() { const inner = "안쪽"; function second() { console.log(inner); // "안쪽" — first의 스코프에서 찾음 console.log(outer); // "바깥" — 전역 스코프에서 찾음 } second(); } first();

중요한 건 스코프 체인이 함수가 호출된 위치가 아니라 선언된 위치를 기준으로 결정된다는 점이다. 이를 **렉시컬 스코프(Lexical Scope)**라고 부른다.

const value = "전역"; function outer() { const value = "outer 내부"; inner(); } function inner() { console.log(value); // "전역" (outer 안에서 호출됐지만, inner가 선언된 위치 기준으로 스코프가 결정됨) } outer();


클로저 (Closure)와 활용 사례

클로저는 함수가 자신이 선언될 당시의 스코프를 기억해서, 함수가 스코프 밖에서 실행되더라도 그 스코프의 변수에 계속 접근할 수 있는 현상을 말한다.

기본 예시

function outer() { let count = 0; function inner() { count++; console.log(count); } return inner; } const counter = outer(); counter(); // 1 counter(); // 2 counter(); // 3

outer 함수는 이미 실행이 끝났는데도, 반환된 inner 함수는 여전히 count 변수에 접근하고 값을 변경할 수 있다. 이게 가능한 이유는 inner 함수가 자신이 선언된 스코프(outer의 스코프)를 기억하고 있기 때문이다.

활용 사례 1 — 상태 은닉 (캡슐화)

외부에서 직접 접근할 수 없는 비공개 변수를 만들 때 사용한다.

function createCounter() { let count = 0; return { increment() { count++; return count; }, decrement() { count--; return count; }, getCount() { return count; } }; } const counter = createCounter(); console.log(counter.increment()); // 1 console.log(counter.increment()); // 2 console.log(counter.count); // undefined (외부에서 직접 접근 불가)

count는 createCounter 함수 스코프 안에만 존재해서, 외부에서는 오직 반환된 메서드를 통해서만 값을 다룰 수 있다.

활용 사례 2 — 함수 팩토리

특정 값을 고정해두고, 그 값만 다르게 적용한 여러 함수를 만들 때 사용한다.

function multiplyBy(x) { return function (y) { return x * y; }; } const double = multiplyBy(2); const triple = multiplyBy(3); console.log(double(10)); // 20 console.log(triple(10)); // 30

활용 사례 3 — 이벤트 핸들러에서 상태 유지

function setupButton() { let clickCount = 0; document.querySelector("button").addEventListener("click", function () { clickCount++; console.log(`${clickCount}번 클릭됨`); }); }

주의할 점 — 반복문과 클로저

var로 반복문을 돌리면서 클로저를 만들면 의도치 않은 결과가 나올 수 있다.

for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 100); } // 3 3 3 (모두 같은 i를 참조하기 때문)

var는 함수 스코프라서 반복문이 다 끝난 뒤의 i 값(3)을 모든 콜백이 공유해서 참조한다. let으로 바꾸면 매 반복마다 새로운 블록 스코프가 생겨서 의도한 대로 동작한다.

for (let i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 100); } // 0 1 2


this 바인딩

this는 함수가 호출되는 방식에 따라 가리키는 대상이 달라진다. 함수가 선언된 위치가 아니라 호출 시점에 결정된다는 점이 스코프와 가장 큰 차이다.

전역에서의 this

console.log(this); // 브라우저 환경에서는 window 객체

객체 메서드에서의 this

메서드를 호출한 객체를 가리킨다.

const user = { name: "고리", greet() { console.log(this.name); } }; user.greet(); // "고리" (user가 this)

단, 메서드를 변수에 따로 담아서 호출하면 this가 유지되지 않는다.

const greet = user.greet; greet(); // undefined (this가 user를 가리키지 않음)

일반 함수에서의 this

일반 함수로 호출하면 this는 전역 객체를 가리킨다. (strict mode에서는 undefined)

function showThis() { console.log(this); } showThis(); // window (또는 strict mode에서는 undefined)

화살표 함수에서의 this

화살표 함수는 자신만의 this를 가지지 않는다. 대신 자신이 선언된 위치의 상위 스코프 this를 그대로 물려받는다. 즉 this 바인딩도 스코프처럼 렉시컬(선언 위치 기준)하게 동작한다.

const user = { name: "고리", greet: () => { console.log(this.name); // 화살표 함수는 user의 메서드로 호출돼도 자기만의 this가 없음 } }; user.greet(); // undefined (전역의 this를 그대로 사용)

이 특성은 오히려 콜백 함수 안에서 유용하게 쓰인다.

const timer = { seconds: 0, start() { setInterval(() => { this.seconds++; // 화살표 함수라서 start를 호출한 timer의 this를 그대로 사용 console.log(this.seconds); }, 1000); } }; timer.start(); // 1 2 3 ...

만약 일반 함수로 썼다면 setInterval의 콜백은 일반 함수 호출 방식으로 실행되기 때문에 this가 timer를 가리키지 못했을 것이다.

explicit binding — call / apply / bind

this를 개발자가 명시적으로 지정하고 싶을 때 사용하는 메서드들이다.

const user1 = { name: "고리" }; const user2 = { name: "영희" }; function greet(greeting) { console.log(`${greeting}, ${this.name}`); } // call: 인자를 하나씩 나열 greet.call(user1, "안녕"); // 안녕, 고리 // apply: 인자를 배열로 전달 greet.apply(user2, ["안녕"]); // 안녕, 영희 // bind: this가 고정된 새로운 함수를 반환 (즉시 실행하지 않음) const greetUser1 = greet.bind(user1); greetUser1("반가워"); // 반가워, 고리
메서드실행 시점인자 전달 방식
call즉시 실행하나씩 나열
apply즉시 실행배열로 전달
bind실행하지 않고 새 함수 반환하나씩 나열

this 정리 — 바인딩 규칙 우선순위

  1. new 키워드로 호출 (생성자 함수) → 새로 생성된 객체
  2. call/apply/bind로 명시적 바인딩 → 지정한 객체
  3. 객체의 메서드로 호출 → 호출한 객체
  4. 화살표 함수 → 선언 위치의 상위 스코프 this
  5. 그 외 일반 함수 호출 → 전역 객체 (strict mode에서는 undefined)


마지막 수정