Block

JavaScript에서 블록 스코프(Block Scope) 는 중괄호({})로 감싸진 코드 블록 내에서 선언된 변수나 함수가 해당 블록 내부에서만 유효한 범위를 의미한다.
이는 코드의 구조와 가독성, 유지보수성에 큰 영향을 미치며, 변수의 생명 주기와 가시성을 결정짓는 중요한 개념이다.

블록 스코프는 JavaScript에서 변수의 가시성과 생명주기를 제어하는 강력한 개념이다:

  • ES6에서 letconst 키워드를 통해 도입되었다.
  • 중괄호({})로 둘러싸인 코드 블록 내에서만 변수에 접근할 수 있다.
  • 메모리 효율성, 코드 구조화, 버그 방지에 도움이 된다.
  • 일시적 사각지대(TDZ)를 통해 더 예측 가능한 변수 동작을 제공한다.
  • 클로저와 결합하여 강력한 패턴을 구현할 수 있다.

JavaScript에서 블록은 다음과 같은 상황에서 생성된다:

  • if
  • for, while 등의 반복문
  • switch
  • 단독 블록 {}
  • try/catch/finally

예를 들어, 다음과 같은 코드에서 중괄호 안에 있는 영역이 블록이다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (true) {
  // 이것이 블록입니다
}

for (let i = 0; i < 5; i++) {
  // 이것도 블록입니다
}

{
  // 독립적인 블록도 가능합니다
}

블록 스코프의 역사적 배경

JavaScript의 블록 스코프는 비교적 새로운 개념이다.
ES6(ECMAScript 2015) 이전에는 JavaScript에서 진정한 의미의 블록 스코프가 존재하지 않았으며, ES6(ECMAScript 2015)에서 let과 const 키워드와 함께 도입되었다.

  1. ES5 이전: var와 함수 스코프
    ES5까지 JavaScript에서는 var 키워드로 선언된 변수가 함수 스코프 또는 전역 스코프를 가졌다.
    블록은 스코프를 생성하지 않았다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    function oldExample() {
      var x = 1;
    
      if (true) {
        var x = 2; // 같은 함수 스코프의 x를 재할당
        console.log(x); // 2
      }
    
      console.log(x); // 2 (블록 외부에서도 값이 변경됨)
    }
    

    이러한 동작은 다른 프로그래밍 언어와 달랐고, 예기치 않은 버그의 원인이 되었다.

  2. ES6의 도입: Let과 Const
    ES6에서는 letconst 키워드가 도입되어 진정한 블록 스코프를 지원하게 되었다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    function newExample() {
      let x = 1;
    
      if (true) {
        let x = 2; // 새로운 블록 스코프에 x 생성
        console.log(x); // 2
      }
    
      console.log(x); // 1 (블록 외부의 x는 변경되지 않음)
    }
    

Let과 Const의 블록 스코프 특성

  1. Let 키워드
    let으로 선언된 변수는 블록 스코프를 가진다:

    1
    2
    3
    4
    5
    6
    
    {
      let blockScoped = '블록 내부에서만 접근 가능';
      console.log(blockScoped); // '블록 내부에서만 접근 가능'
    }
    
    // console.log(blockScoped); // ReferenceError: blockScoped is not defined
    

    let은 다음과 같은 특성을 갖는다:
    - 선언된 블록 내에서만 접근 가능
    - 같은 스코프 내에서 중복 선언 불가
    - 값 재할당 가능
    - 호이스팅은 되지만, 초기화 전에 접근하면 에러 발생 (일시적 사각지대)

  2. Const 키워드
    constlet과 동일한 블록 스코프를 가지지만, 값을 재할당할 수 없다:

    1
    2
    3
    4
    5
    6
    7
    8
    
    {
      const PI = 3.14159;
      console.log(PI); // 3.14159
    
      // PI = 3; // TypeError: Assignment to constant variable
    }
    
    // console.log(PI); // ReferenceError: PI is not defined
    

    const의 주요 특성:
    - let과 동일한 블록 스코프 규칙
    - 선언과 동시에 초기화 필수
    - 값 재할당 불가
    - 객체나 배열이 할당된 경우, 내부 속성은 변경 가능

    1
    2
    3
    4
    5
    6
    7
    
    {
      const user = { name: '홍길동' };
      user.name = '김철수'; // 객체 속성 변경 가능
      console.log(user.name); // '김철수'
    
      // user = {}; // TypeError: Assignment to constant variable
    }
    

블록 스코프의 중첩

블록은 중첩될 수 있으며, 내부 블록에서는 외부 블록의 변수에 접근할 수 있지만 외부 블록에서는 내부 블록의 변수에 접근할 수 없다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  let outer = '외부 블록';
  
  {
    let inner = '내부 블록';
    console.log(outer); // '외부 블록' (접근 가능)
    console.log(inner); // '내부 블록'
  }
  
  console.log(outer); // '외부 블록'
  // console.log(inner); // ReferenceError: inner is not defined
}

블록 스코프와 일시적 사각지대(TDZ)

블록 스코프에서 중요한 개념 중 하나가 ‘일시적 사각지대(Temporal Dead Zone, TDZ)‘이다.
이는 변수가 선언되었지만 아직 초기화되지 않은 스코프 영역이다:

1
2
3
4
5
6
7
8
{
  // 여기서부터 TDZ 시작
  
  // console.log(blockVar); // ReferenceError: Cannot access 'blockVar' before initialization
  
  let blockVar = '초기화 완료'; // TDZ 종료
  console.log(blockVar); // '초기화 완료'
}

TDZ는 다음과 같은 특성을 갖는다:

  • 블록의 시작부터 변수 선언문까지의 영역
  • 이 영역에서 변수에 접근하면 ReferenceError 발생
  • let, const, class 선언에 적용됨
  • var에는 적용되지 않음

반복문에서의 블록 스코프

반복문의 블록 스코프는 특히 유용하다.
각 반복마다 새로운 스코프가 생성된다:

  1. For 반복문

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    for (let i = 0; i < 3; i++) {
      // 각 반복마다 새로운 i가 생성됨
      setTimeout(() => console.log(i), 100); // 0, 1, 2가 올바르게 출력
    }
    
    // 비교: var를 사용한 경우
    for (var j = 0; j < 3; j++) {
      // 모든 반복에서 같은 j를 사용
      setTimeout(() => console.log(j), 100); // 3, 3, 3 출력 (반복 종료 후의 값)
    }
    
  2. for…of, for…in 반복문

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    const fruits = ['사과', '바나나', '오렌지'];
    
    for (let fruit of fruits) {
      // 각 반복마다 새로운 fruit 변수 스코프
      console.log(fruit);
    }
    // console.log(fruit); // ReferenceError: fruit is not defined
    
    const user = { name: '홍길동', age: 30, job: '개발자' };
    
    for (let key in user) {
      // 각 반복마다 새로운 key 변수 스코프
      console.log(`${key}: ${user[key]}`);
    }
    // console.log(key); // ReferenceError: key is not defined
    

블록 스코프와 클로저의 관계

블록 스코프는 클로저 생성에 중요한 역할을 한다.
클로저는 함수가 자신이 생성된 렉시컬 환경을 기억하는 특성을 말한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function createFunctions() {
  const functions = [];
  
  for (let i = 0; i < 3; i++) {
    // 각 반복마다 새로운 i가 생성되어 각 클로저에 독립적으로 캡처됨
    functions.push(function() {
      console.log(i);
    });
  }
  
  return functions;
}

const [f0, f1, f2] = createFunctions();
f0(); // 0
f1(); // 1
f2(); // 2

// var를 사용했다면 모두 3을 출력했을 것

변수 섀도잉

블록 내에서 외부 스코프와 동일한 이름의 변수를 선언할 수 있으며, 이는 외부 변수를 가리게 된다.

1
2
3
4
5
6
let x = "outer";
if (true) {
  let x = "inner";
  console.log(x); // "inner"
}
console.log(x); // "outer"

블록 스코프를 활용한 실용적인 패턴

  1. 임시 변수 격리
    블록 스코프를 사용하여 임시 변수를 격리할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    function processData(data) {
      // 데이터 처리 로직
    
      {
        // 임시 변수를 위한 별도 블록
        let temp = data.slice();
        temp.sort();
        temp = temp.filter(item => item > 0);
    
        // 필요한 계산 수행
        const result = calculateAverage(temp);
        data.average = result;
      }
      // temp는 여기서 접근 불가
    
      return data;
    }
    
  2. 단독 블록을 이용한 변수 스코프 제한
    파일 최상위 레벨에서도 블록을 사용하여 변수의 스코프를 제한할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    // 전역 변수를 최소화하기 위한 패턴
    {
      const API_KEY = 'secret_key';
      const API_URL = 'https://api.example.com';
    
      function fetchData() {
        // API_KEY와 API_URL을 사용
      }
    
      // 필요한 함수나 변수만 전역으로 노출
      window.fetchData = fetchData;
    }
    
    // API_KEY와 API_URL은 여기서 접근 불가
    
  3. Switch문에서의 변수 선언
    switch문에서도 블록 스코프가 유용하하다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    switch (status) {
      case 'success': {
        let message = '성공적으로 처리되었습니다';
        console.log(message);
        break;
      }
      case 'error': {
        let message = '오류가 발생했습니다';
        console.log(message);
        break;
      }
      default: {
        let message = '처리 중입니다';
        console.log(message);
      }
    }
    
    // 각 case에서 동일한 변수명 message를 사용할 수 있음
    // console.log(message); // ReferenceError: message is not defined
    

블록 스코프와 성능

블록 스코프는 성능에도 영향을 미칠 수 있다:

  1. 메모리 사용 최적화
    변수가 필요한 범위에서만 살아있도록 하여 메모리 사용을 최적화할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    function processManyItems(items) {
      // 처리 로직
    
      for (let i = 0; i < items.length; i++) {
        // 각 항목에 대한 대규모 처리
        {
          // 대용량 데이터를 위한 임시 변수
          let tempData = new Array(10000).fill(items[i]);
          let processed = heavyProcessing(tempData);
          items[i] = processed.result;
        }
        // tempData와 processed는 여기서 가비지 컬렉션의 대상이 됨
      }
    
      return items;
    }
    
  2. 엔진 최적화 기회 제공
    블록 스코프를 사용하면 JavaScript 엔진이 변수의 생명주기를 더 정확히 파악하여 최적화할 수 있다.

블록 스코프 관련 주의사항과 모범 사례

try/catch 블록

try/catch 문에서 catch 블록은 오류 객체에 대한 별도의 스코프를 생성한다:

1
2
3
4
5
6
7
8
9
try {
  // 시도하는 코드
  throw new Error('오류 발생');
} catch (error) {
  // error는 catch 블록 내에서만 접근 가능
  console.log(error.message); // '오류 발생'
}

// console.log(error); // ReferenceError: error is not defined

블록 스코프 모범 사례

  1. 변수의 스코프를 최소화하기

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // 좋은 예: 변수를 필요한 스코프로 제한
    function processUserData(userId) {
      // 사용자 데이터 가져오기
    
      if (userId > 0) {
        let userData = fetchUserData(userId);
        processData(userData);
      }
    
      // userData는 여기서 접근 불가 (메모리 효율성)
    }
    
  2. const를 기본으로 사용하기

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // 값이 변경되지 않는 변수는 const 사용
    {
      const CONFIG = {
        apiUrl: 'https://api.example.com',
        timeout: 5000
      };
    
      // 변경이 필요한 경우에만 let 사용
      let retryCount = 3;
    
      // 로직 수행...
    }
    
  3. 블록을 활용하여 코드 구조화하기

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    function complexFunction() {
      // 초기화 관련 로직
      {
        // 1단계: 데이터 준비
        const data = prepareData();
        validateData(data);
      }
    
      {
        // 2단계: 비즈니스 로직 처리
        const result = processBusinessLogic();
        verifyResult(result);
      }
    
      {
        // 3단계: 마무리 작업
        const summary = createSummary();
        return summary;
      }
    }
    
  4. 명시적인 블록 주석 사용하기

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    function largeFunction() {
      // --- 초기화 섹션 ---
      {
        // 초기화 관련 코드...
      }
    
      // --- 데이터 처리 섹션 ---
      {
        // 데이터 처리 관련 코드...
      }
    
      // --- 결과 반환 섹션 ---
      {
        // 결과 반환 관련 코드...
      }
    }
    

블록 스코프의 실제 사용 예제

웹 애플리케이션에서의 이벤트 핸들러

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function setupEventHandlers() {
  const buttons = document.querySelectorAll('.action-button');
  
  buttons.forEach(button => {
    // 각 버튼에 대한 데이터 설정
    const buttonId = button.getAttribute('data-id');
    
    // 이벤트 리스너 등록
    button.addEventListener('click', () => {
      // 블록 스코프 변수를 캡처하는 클로저
      console.log(`Button ${buttonId} clicked`);
      
      {
        // 클릭 시 임시 상태 관리
        let isProcessing = true;
        button.classList.add('processing');
        
        processButtonAction(buttonId)
          .then(result => {
            console.log(result);
          })
          .finally(() => {
            button.classList.remove('processing');
            // isProcessing은 이 블록에서만 필요함
          });
      }
    });
  });
}

API 호출에서의 블록 스코프 활용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async function fetchUserData(userId) {
  // API URL 및 옵션 설정
  {
    const API_KEY = getApiKey(); // 민감한 키는 가능한 작은 스코프에 유지
    const url = `https://api.example.com/users/${userId}`;
    
    const options = {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      }
    };
    
    try {
      const response = await fetch(url, options);
      // API_KEY는 이 블록 외부에서 접근 불가
      
      if (!response.ok) {
        throw new Error('API 호출 실패');
      }
      
      return await response.json();
    } catch (error) {
      console.error('사용자 데이터 가져오기 오류:', error);
      throw error;
    }
  }
}

모듈 패턴 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const userModule = (function() {
  // 모듈 내부 상태 (비공개)
  let userState = null;
  
  // 내부 유틸리티 함수
  function validateUser(user) {
    return user && user.id && user.name;
  }
  
  // 공개 API
  return {
    setUser: function(user) {
      // 블록 스코프를 사용한 검증 로직 격리
      {
        const isValid = validateUser(user);
        if (!isValid) {
          throw new Error('유효하지 않은 사용자 데이터');
        }
        
        // 추가 검증 로직
        let permissionsValid = checkPermissions(user);
        if (!permissionsValid) {
          throw new Error('권한이 유효하지 않음');
        }
      }
      
      // 검증 통과 후 상태 업데이트
      userState = { ...user, lastUpdated: new Date() };
      return true;
    },
    
    getUser: function() {
      return userState ? { ...userState } : null;
    }
  };
})();

var와 블록 스코프의 차이

varlet/const의 블록 스코프 차이를 명확히 이해하는 것이 중요하다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function varvs.Let() {
  var varArray = [];
  let letArray = [];
  
  // var의 함수 스코프 vs. let의 블록 스코프
  for (var i = 0; i < 3; i++) {
    varArray.push(function() { return i; });
  }
  
  for (let j = 0; j < 3; j++) {
    letArray.push(function() { return j; });
  }
  
  console.log(varArray.map(f => f())); // [3, 3, 3]
  console.log(letArray.map(f => f())); // [0, 1, 2]
  
  // 블록 내 var 선언은 함수 스코프에 적용됨
  {
    var varInBlock = 'var in block';
    let letInBlock = 'let in block';
  }
  
  console.log(varInBlock); // 'var in block'
  // console.log(letInBlock); // ReferenceError: letInBlock is not defined
  
  // if 문에서도 동일
  if (true) {
    var varInIf = 'var in if';
    let letInIf = 'let in if';
  }
  
  console.log(varInIf); // 'var in if'
  // console.log(letInIf); // ReferenceError: letInIf is not defined
}

블록 스코프와 브라우저 호환성

블록 스코프(letconst)는 비교적 최신 기능이므로 호환성을 고려해야 한다:

  • IE11 이하: 지원하지 않음
  • Edge 12+, Firefox 44+, Chrome 49+, Safari 10+: 완벽 지원
  • 구형 브라우저 지원을 위해서는 Babel과 같은 트랜스파일러 사용 필요

블록 스코프의 디버깅과 도구 활용

  1. 크롬 개발자 도구에서의 블록 스코프 확인
    크롬 개발자 도구의 Sources 패널에서 디버거를 사용하면 블록 스코프를 시각적으로 확인할 수 있다:

    1. 중단점(breakpoint)을 설정한다.
    2. 코드 실행 시 중단점에서 일시 중지된다.
    3. Scope 패널에서 현재 스코프의 변수를 확인한다:
      • Block Scope: 현재 블록의 변수
      • Closure: 상위 블록의 변수
      • Script: 스크립트 스코프 변수
      • Global: 전역 변수
  2. ESLint를 활용한 스코프 관련 문제 방지
    ESLint 설정을 통해 블록 스코프 관련 모범 사례를 강제할 수 있다:

    1
    2
    3
    4
    5
    6
    7
    8
    
    {
      "rules": {
        "no-var": "error",           // var 사용 금지
        "prefer-const": "error",     // 재할당되지 않는 변수는 const 사용
        "block-scoped-var": "error", // 블록 스코프 변수처럼 var 사용
        "no-shadow": "error"         // 상위 스코프 변수명 재사용 금지
      }
    }
    

참고 및 출처