Core JavaScript 4-2 주차

Junyeol·2025년 8월 25일

Core JavaScript

목록 보기
5/8
post-thumbnail

JavaScript 스코프와 스코프 체인 완벽 가이드

스코프(Scope)란?

스코프란 식별자(변수명)가 유효한 범위를 말합니다. 즉, 변수를 어디서 참조할 수 있는지를 결정하는 규칙이에요.

JavaScript에서 스코프는 함수 단위로 생성되며, 이는 다른 언어와 구분되는 중요한 특징입니다.

var 전역변수 = '어디서든 접근 가능';

function 외부함수() {
  var 외부변수 = '외부 함수 내부에서만 접근';
  
  function 내부함수() {
    var 내부변수 = '내부 함수에서만 접근';
    
    console.log(전역변수);  // ✅ 접근 가능
    console.log(외부변수);  // ✅ 접근 가능
    console.log(내부변수);  // ✅ 접근 가능
  }
  
  console.log(전역변수);  // ✅ 접근 가능
  console.log(외부변수);  // ✅ 접근 가능
  console.log(내부변수);  // ❌ ReferenceError!
}

console.log(전역변수);  // ✅ 접근 가능
console.log(외부변수);  // ❌ ReferenceError!
console.log(내부변수);  // ❌ ReferenceError!

스코프의 종류

1. 전역 스코프 (Global Scope)

var 전역변수 = 'I am global';
let 전역렛 = 'I am also global';
const 전역상수 = 'I am global constant';

function 어디서든접근() {
  console.log(전역변수);  // 'I am global'
  console.log(전역렛);    // 'I am also global'
  console.log(전역상수);  // 'I am global constant'
}

if (true) {
  console.log(전역변수);  // 'I am global'
  console.log(전역렛);    // 'I am also global'
  console.log(전역상수);  // 'I am global constant'
}

전역 스코프의 특징:

  • 프로그램 전체에서 접근 가능
  • 브라우저에서는 window 객체의 프로퍼티가 됨
  • 남용하면 네임스페이스 오염 문제 발생

2. 함수 스코프 (Function Scope)

function 함수스코프예제() {
  var 함수변수 = '함수 내부 변수';
  let 함수렛 = '함수 내부 let';
  const 함수상수 = '함수 내부 const';
  
  if (true) {
    var 블록안var = 'var는 함수 스코프';
    let 블록안let = 'let은 블록 스코프';
    const 블록안const = 'const도 블록 스코프';
  }
  
  console.log(함수변수);    // ✅ '함수 내부 변수'
  console.log(함수렛);      // ✅ '함수 내부 let'
  console.log(함수상수);    // ✅ '함수 내부 const'
  console.log(블록안var);   // ✅ 'var는 함수 스코프'
  console.log(블록안let);   // ❌ ReferenceError!
  console.log(블록안const); // ❌ ReferenceError!
}

함수스코프예제();

// 함수 밖에서는 접근 불가
console.log(함수변수);  // ❌ ReferenceError!

핵심: var는 함수 스코프를 따르지만, let과 const는 블록 스코프를 따릅니다.

3. 블록 스코프 (Block Scope) - ES6+

{
  var 블록var = 'var는 블록을 무시';
  let 블록let = 'let은 블록에 갇힘';
  const 블록const = 'const도 블록에 갇힘';
}

console.log(블록var);    // ✅ 'var는 블록을 무시'
console.log(블록let);    // ❌ ReferenceError!
console.log(블록const);  // ❌ ReferenceError!

// for문에서의 차이
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log('var:', i), 100); // 3, 3, 3
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log('let:', j), 100); // 0, 1, 2
}

스코프 체인 (Scope Chain)

스코프 체인이란 식별자를 찾기 위해 스코프를 연쇄적으로 탐색하는 메커니즘입니다.

var 전역변수 = '전역';

function 단계1() {
  var 변수 = '단계1';
  
  function 단계2() {
    var 변수 = '단계2';
    
    function 단계3() {
      console.log(변수); // 어떤 값이 출력될까요?
    }
    
    단계3();
  }
  
  단계2();
}

단계1(); // '단계2' 출력

스코프 체인 탐색 과정:
1. 현재 스코프(단계3)에서 변수 찾기 → 없음
2. 상위 스코프(단계2)에서 변수 찾기 → 발견! ('단계2')
3. 더 이상 탐색하지 않고 해당 값 반환

스코프 체인의 실제 동작

var 이름 = '전역 철수';

function 외부함수() {
  var 나이 = 30;
  
  function 내부함수() {
    var 취미 = '독서';
    
    // 스코프 체인을 따라 변수 탐색
    console.log(취미);  // 내부함수 스코프에서 발견
    console.log(나이);  // 외부함수 스코프에서 발견
    console.log(이름);  // 전역 스코프에서 발견
    
    // 없는 변수를 참조하면?
    console.log(존재하지않는변수); // ReferenceError!
  }
  
  내부함수();
}

외부함수();

렉시컬 스코프 (Lexical Scope)

JavaScript는 렉시컬 스코프를 따릅니다. 이는 함수가 호출되는 위치가 아닌, 함수가 선언된 위치에 따라 스코프가 결정된다는 의미입니다.

var 글로벌 = '전역 변수';

function 함수1() {
  var 지역 = '함수1 지역 변수';
  함수2();
}

function 함수2() {
  console.log(글로벌);  // ✅ 접근 가능 (전역에서 선언되었으니)
  console.log(지역);    // ❌ ReferenceError! (함수2가 선언된 위치에서 지역 변수가 없음)
}

함수1();

핵심: 함수2가 함수1 내부에서 호출되었지만, 함수2는 전역에서 선언되었기 때문에 전역 스코프만 참조할 수 있습니다.

렉시컬 스코프 vs 동적 스코프 비교

var 변수 = '전역';

function 렉시컬예제() {
  var 변수 = '렉시컬';
  
  function 내부함수() {
    console.log(변수); // '렉시컬' - 선언 위치 기준
  }
  
  내부함수();
  return 내부함수;
}

function 다른함수() {
  var 변수 = '다른함수';
  var 가져온함수 = 렉시컬예제();
  가져온함수(); // 여전히 '렉시컬' - 호출 위치와 무관
}

다른함수();

변수 호이스팅과 스코프

var의 호이스팅

console.log(변수); // undefined (선언은 호이스팅, 초기화는 안됨)

var 변수 = '할당됨';

console.log(변수); // '할당됨'

// 실제로는 이렇게 동작
var 변수; // undefined로 초기화 (호이스팅)
console.log(변수); // undefined
변수 = '할당됨';
console.log(변수); // '할당됨'

let/const의 호이스팅과 TDZ

console.log(렛변수); // ❌ ReferenceError! (TDZ)
console.log(상수);   // ❌ ReferenceError! (TDZ)

let 렛변수 = 'let 값';
const 상수 = 'const 값';

console.log(렛변수); // 'let 값'
console.log(상수);   // 'const 값'

TDZ (Temporal Dead Zone): let/const는 선언 전까지 접근할 수 없는 영역이 존재합니다.

클로저와 스코프

클로저는 스코프 체인을 활용한 JavaScript의 강력한 기능입니다.

function 외부함수(외부변수) {
  
  function 내부함수(내부변수) {
    console.log('외부변수:', 외부변수);
    console.log('내부변수:', 내부변수);
  }
  
  return 내부함수;
}

const 클로저함수 = 외부함수('외부에서 전달');
클로저함수('내부에서 전달');
// 외부변수: 외부에서 전달
// 내부변수: 내부에서 전달

클로저의 실용적 예제:

function 카운터만들기() {
  let 카운트 = 0;
  
  return {
    증가: function() {
      카운트++;
      return 카운트;
    },
    감소: function() {
      카운트--;
      return 카운트;
    },
    현재값: function() {
      return 카운트;
    }
  };
}

const 카운터1 = 카운터만들기();
const 카운터2 = 카운터만들기();

console.log(카운터1.증가()); // 1
console.log(카운터1.증가()); // 2
console.log(카운터2.증가()); // 1 (독립적인 스코프)

console.log(카운터1.현재값()); // 2
console.log(카운터2.현재값()); // 1

실무에서 자주 만나는 스코프 문제들

1. 반복문에서의 클로저 문제

// 문제가 있는 코드
var 버튼들 = [];
for (var i = 0; i < 3; i++) {
  버튼들[i] = function() {
    console.log('버튼', i, '클릭!'); // 모두 3을 출력
  };
}

버튼들[0](); // '버튼 3 클릭!'
버튼들[1](); // '버튼 3 클릭!'
버튼들[2](); // '버튼 3 클릭!'

// 해결책 1: let 사용
const 올바른버튼들1 = [];
for (let j = 0; j < 3; j++) {
  올바른버튼들1[j] = function() {
    console.log('버튼', j, '클릭!');
  };
}

올바른버튼들1[0](); // '버튼 0 클릭!'
올바른버튼들1[1](); // '버튼 1 클릭!'
올바른버튼들1[2](); // '버튼 2 클릭!'

// 해결책 2: 즉시실행함수(IIFE) 사용
const 올바른버튼들2 = [];
for (var k = 0; k < 3; k++) {
  올바른버튼들2[k] = (function(인덱스) {
    return function() {
      console.log('버튼', 인덱스, '클릭!');
    };
  })(k);
}

올바른버튼들2[0](); // '버튼 0 클릭!'
올바른버튼들2[1](); // '버튼 1 클릭!'
올바른버튼들2[2](); // '버튼 2 클릭!'

2. 모듈 패턴과 스코프

const 모듈 = (function() {
  // 비공개 변수들
  let 비밀데이터 = '접근 불가';
  let 카운터 = 0;
  
  // 비공개 함수들
  function 비공개함수() {
    console.log('외부에서 호출 불가');
  }
  
  // 공개 API 반환
  return {
    공개메서드1: function() {
      카운터++;
      console.log('카운터:', 카운터);
    },
    
    공개메서드2: function() {
      비공개함수(); // 내부에서는 호출 가능
      return 비밀데이터.length;
    },
    
    getter: function() {
      return 카운터;
    }
  };
})();

모듈.공개메서드1(); // '카운터: 1'
console.log(모듈.getter()); // 1

// 비공개 요소들은 접근 불가
console.log(모듈.비밀데이터);   // undefined
모듈.비공개함수();             // TypeError!

ES6+ 모듈과 스코프

// math.js 파일
const PI = 3.14159;
let 계산횟수 = 0;

function 원넓이(반지름) {
  계산횟수++;
  return PI * 반지름 * 반지름;
}

function 원둘레(반지름) {
  계산횟수++;
  return 2 * PI * 반지름;
}

// 공개할 것만 export
export { 원넓이, 원둘레 };
export { PI as 파이상수 };

// main.js 파일
import { 원넓이, 원둘레, 파이상수 } from './math.js';

console.log(원넓이(5));     // 78.53975
console.log(파이상수);      // 3.14159
console.log(계산횟수);      // ❌ ReferenceError! (비공개)

스코프 디버깅과 성능 최적화

// 성능에 좋지 않은 예
function 성능나쁨() {
  for (let i = 0; i < 1000; i++) {
    // 반복문 안에서 함수 선언 (매번 새로 생성)
    const 헬퍼함수 = function() {
      return Math.random();
    };
    console.log(헬퍼함수());
  }
}

// 성능에 좋은 예
function 성능좋음() {
  // 함수를 반복문 밖에서 선언
  const 헬퍼함수 = function() {
    return Math.random();
  };
  
  for (let i = 0; i < 1000; i++) {
    console.log(헬퍼함수());
  }
}

// 스코프 체인 최적화
const 전역변수 = '전역값';

function 최적화예제() {
  // 자주 사용되는 전역 변수를 지역 변수로 캐싱
  const 로컬전역변수 = 전역변수;
  
  for (let i = 0; i < 10000; i++) {
    // 스코프 체인을 타고 올라가지 않아도 됨
    console.log(로컬전역변수);
  }
}

핵심 정리

스코프의 기본 규칙:
1. 함수 스코프: var는 함수 단위로 스코프 생성
2. 블록 스코프: let/const는 블록 단위로 스코프 생성
3. 렉시컬 스코프: 함수 선언 위치에 따라 스코프 결정
4. 스코프 체인: 안쪽에서 바깥쪽으로 변수 탐색

호이스팅 특징:

  • var: 선언 호이스팅 + undefined 초기화
  • let/const: 선언 호이스팅 + TDZ(접근 불가 구간)
  • 함수 선언문: 전체 호이스팅
  • 함수 표현식: 변수 호이스팅 규칙 따름

기억하세요: 스코프는 코드 작성 시점에 결정되며, 실행 시점의 호출 위치와는 무관합니다!

profile
천천히

0개의 댓글