Function

JavaScript의 함수 스코프(Function Scope)는 함수 내에서 선언된 변수의 가시성과 접근성을 정의하는 중요한 개념이다. 이 스코프는 JavaScript의 변수 관리 및 코드 구조에 큰 영향을 미친다.

함수 스코프의 정의

함수 스코프란 함수 내부에 선언된 변수와 함수가 해당 함수 내부에서만 접근 가능하다는 JavaScript의 특성을 의미한다.
이는 함수가 자신만의 독립적인 변수 환경을 가지고 있음을 뜻한다.
함수가 실행될 때 생성되고, 함수가 종료되면 메모리에서 사라진다.

1
2
3
4
5
6
7
8
9
function exampleFunction() {
  // 이 변수는 함수 내부에서만 접근 가능
  var functionScopedVar = "함수 스코프 내부 변수";
  
  console.log(functionScopedVar); // "함수 스코프 내부 변수"
}

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

위 예시에서 functionScopedVarexampleFunction 내부에서만 존재하며, 함수 외부에서는 접근할 수 없다.

함수 스코프와 전역 스코프의 관계

JavaScript에서 함수 외부에 선언된 변수는 전역 스코프(global scope)에 속한다. 전역 변수는 어떤 함수 내에서도 접근할 수 있다.

1
2
3
4
5
6
7
8
9
// 전역 스코프에 선언된 변수
var globalVar = "전역 변수";

function accessGlobal() {
  console.log(globalVar); // "전역 변수"
}

accessGlobal();
console.log(globalVar); // "전역 변수"

함수 내부에서는 전역 변수와 동일한 이름의 지역 변수를 선언할 수 있으며, 이때 지역 변수는 전역 변수를 가린다(shadowing).

1
2
3
4
5
6
7
8
9
var name = "전역 이름";

function showName() {
  var name = "지역 이름"; // 전역 변수 name을 가림
  console.log(name); // "지역 이름"
}

showName();
console.log(name); // "전역 이름" (전역 변수는 변경되지 않음)

변수 선언 키워드와 함수 스코프

JavaScript에서는 변수를 선언하는 세 가지 키워드(var, let, const)가 있으며, 각각 다른 스코프 특성을 가진다.

Var와 함수 스코프

var로 선언된 변수는 함수 스코프를 가진다. 즉, 변수는 선언된 함수 내부 전체에서 접근 가능하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function varExample() {
  var x = 1;
  
  if (true) {
    var x = 2; // 같은 함수 내의 x를 재할당
    console.log(x); // 2
  }
  
  console.log(x); // 2 (if 블록 밖에서도 변경된 값이 유지됨)
}

varExample();

이러한 특성으로 인해 var는 예상치 못한 동작을 일으킬 수 있다.

Let, Const와 블록 스코프

ES6에서 도입된 letconst는 블록 스코프를 가진다.
이들은 함수 스코프보다 더 제한적인 스코프를 제공한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function letConstExample() {
  let x = 1;
  const y = 2;
  
  if (true) {
    let x = 3; // 새로운 블록 스코프에 x 선언
    const y = 4; // 새로운 블록 스코프에 y 선언
    console.log(x, y); // 3, 4
  }
  
  console.log(x, y); // 1, 2 (바깥쪽 변수는 영향받지 않음)
}

letConstExample();

함수 스코프와 호이스팅(Hoisting)

호이스팅은 JavaScript 엔진이 실행 전에 변수와 함수 선언을 해당 스코프의 맨 위로 끌어올리는 것처럼 동작하는 메커니즘이다.

변수 호이스팅

var로 선언된 변수는 함수 스코프의 최상단으로 호이스팅되지만, 초기화는 원래 위치에서 이루어진다.

1
2
3
4
5
6
7
8
function hoistingExample() {
  console.log(hoistedVar); // undefined (오류는 발생하지 않음)
  
  var hoistedVar = "초기화 후";
  console.log(hoistedVar); // "초기화 후"
}

hoistingExample();

위 코드는 내부적으로 다음과 같이 해석된다:

1
2
3
4
5
6
7
function hoistingExample() {
  var hoistedVar; // 선언이 최상단으로 호이스팅됨
  console.log(hoistedVar); // undefined
  
  hoistedVar = "초기화 후"; // 초기화는 원래 위치에서 발생
  console.log(hoistedVar); // "초기화 후"
}

함수 호이스팅

함수 선언(function declaration)은 전체가 호이스팅되어 선언 전에도 호출할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function functionHoistingExample() {
  // 함수 호출이 선언보다 앞에 있음
  console.log(hoistedFunction()); // "함수가 호이스팅됨"
  
  // 함수 선언
  function hoistedFunction() {
    return "함수가 호이스팅됨";
  }
}

functionHoistingExample();

반면, 함수 표현식(function expression)은 변수 호이스팅 규칙을 따른다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function functionExpressionExample() {
  // console.log(functionExpression()); // TypeError: functionExpression is not a function
  
  // 함수 표현식
  var functionExpression = function() {
    return "함수 표현식";
  };
  
  console.log(functionExpression()); // "함수 표현식"
}

functionExpressionExample();

중첩 함수와 스코프 체인

함수는 다른 함수 내부에 정의될 수 있으며, 이를 중첩 함수(nested functions)라고 한다.
중첩 함수는 자신의 스코프, 바깥 함수의 스코프, 전역 스코프를 모두 접근할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function outerFunction() {
  var outerVar = "외부 함수 변수";
  
  function innerFunction() {
    var innerVar = "내부 함수 변수";
    console.log(outerVar); // "외부 함수 변수"
    console.log(innerVar); // "내부 함수 변수"
  }
  
  innerFunction();
  // console.log(innerVar); // ReferenceError: innerVar is not defined
}

outerFunction();

JavaScript 엔진은 변수를 찾을 때 현재 스코프에서 시작하여 찾지 못하면 바깥쪽 스코프로 이동하며 검색을 계속한다.
이 과정을 스코프 체인(scope chain)이라고 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var global = "전역 변수";

function firstLevel() {
  var first = "첫 번째 레벨 변수";
  
  function secondLevel() {
    var second = "두 번째 레벨 변수";
    
    function thirdLevel() {
      var third = "세 번째 레벨 변수";
      
      console.log(third);  // 현재 스코프에서 찾음
      console.log(second); // 부모 스코프에서 찾음
      console.log(first);  // 조부모 스코프에서 찾음
      console.log(global); // 전역 스코프에서 찾음
    }
    
    thirdLevel();
  }
  
  secondLevel();
}

firstLevel();

함수 스코프와 클로저(Closure)

클로저는 함수와 그 함수가 선언된 렉시컬 환경(lexical environment)의 조합이다.
클로저는 함수가 자신이 생성된 스코프의 변수에 계속 접근할 수 있게 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function createCounter() {
  var count = 0; // 프라이빗 변수
  
  return function() {
    count++; // 외부 함수의 변수에 접근
    return count;
  };
}

var counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

위 예시에서 createCounter 함수는 익명 함수를 반환한다. 이 익명 함수는 createCounter의 지역 변수 count에 접근할 수 있으며, createCounter의 실행이 종료된 후에도 해당 변수는 클로저를 통해 유지된다.

클로저는 다음과 같은 상황에서 유용하다:

  1. 데이터 캡슐화와 정보 은닉
  2. 이벤트 핸들러와 콜백 함수
  3. 팩토리 함수와 모듈 패턴
  4. 커링(currying)과 부분 적용(partial application)

클로저를 이용한 정보 은닉 예제

 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
function createPerson(name, age) {
  // private 변수
  var _name = name;
  var _age = age;
  
  // public 인터페이스를 포함한 객체 반환
  return {
    getName: function() {
      return _name;
    },
    getAge: function() {
      return _age;
    },
    setAge: function(newAge) {
      if (newAge > 0 && newAge < 120) {
        _age = newAge;
      }
    }
  };
}

var person = createPerson("홍길동", 30);
console.log(person.getName()); // "홍길동"
console.log(person.getAge());  // 30
person.setAge(31);
console.log(person.getAge());  // 31
// console.log(person._age);   // undefined (직접 접근 불가)

즉시 실행 함수 표현식(IIFE)

즉시 실행 함수 표현식(Immediately Invoked Function Expression, IIFE)은 정의되자마자 실행되는 함수이다.
IIFE는 독립적인 함수 스코프를 생성하여 변수 충돌을 방지하는 데 유용하다.

1
2
3
4
5
6
7
(function() {
  // IIFE 내부의 변수는 외부에서 접근 불가
  var privateVar = "비공개 변수";
  console.log(privateVar); // "비공개 변수"
})();

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

IIFE와 모듈 패턴

모듈 패턴은 IIFE와 클로저를 결합하여 프라이빗 변수와 메소드를 가진 모듈을 생성하는 패턴이다.

 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
var calculator = (function() {
  // 프라이빗 변수
  var result = 0;
  
  // 프라이빗 함수
  function validate(n) {
    return typeof n === "number";
  }
  
  // 퍼블릭 인터페이스
  return {
    add: function(n) {
      if (validate(n)) {
        result += n;
      }
      return this;
    },
    subtract: function(n) {
      if (validate(n)) {
        result -= n;
      }
      return this;
    },
    getResult: function() {
      return result;
    }
  };
})();

calculator.add(5).subtract(2);
console.log(calculator.getResult()); // 3
// console.log(calculator.result);   // undefined (직접 접근 불가)

함수 스코프와 비동기 프로그래밍

비동기 프로그래밍에서 함수 스코프와 클로저는 중요한 역할을 한다.
특히 for 루프 내에서 비동기 함수를 사용할 때 함수 스코프를 이해하는 것이 중요하다.

Var와 비동기 함수의 문제점

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function asyncVarProblem() {
  for (var i = 0; i < 3; i++) {
    // 1초 후에 i를 출력하는 비동기 함수
    setTimeout(function() {
      console.log(i);
    }, 1000);
  }
}

asyncVarProblem(); // 3, 3, 3 출력 (예상과 다름)

위 코드에서 var i는 함수 스코프를 가지므로, 모든 setTimeout 콜백은 같은 i 변수를 참조한다.
루프가 완료될 때 i는 3이 되므로, 모든 콜백은 3을 출력한다.

IIFE로 문제 해결하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function asyncVarSolutionWithIIFE() {
  for (var i = 0; i < 3; i++) {
    // IIFE로 각 반복마다 새로운 스코프 생성
    (function(index) {
      setTimeout(function() {
        console.log(index);
      }, 1000);
    })(i);
  }
}

asyncVarSolutionWithIIFE(); // 0, 1, 2 출력

Let으로 문제 해결하기

ES6의 let은 블록 스코프를 가지므로 문제를 보다 간단하게 해결할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function asyncLetSolution() {
  for (let i = 0; i < 3; i++) {
    // let은 각 반복마다 새로운 i를 생성
    setTimeout(function() {
      console.log(i);
    }, 1000);
  }
}

asyncLetSolution(); // 0, 1, 2 출력

Var, Let, Const의 함수 스코프 비교

세 가지 변수 선언 키워드의 스코프 특성

Var: 함수 스코프

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function varScopeExample() {
  var x = 1;
  
  function innerFunc() {
    var y = 2;
    console.log(x); // 1 (외부 함수의 변수에 접근 가능)
  }
  
  if (true) {
    var x = 3; // 같은 함수 내의 x를 재할당
    var z = 4; // 함수 스코프에 z 추가
  }
  
  console.log(x); // 3 (if 블록에서 변경됨)
  console.log(z); // 4 (if 블록 외부에서도 접근 가능)
  innerFunc();
}

varScopeExample();

Let과 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
function letConstScopeExample() {
  let x = 1;
  const y = 2;
  
  function innerFunc() {
    let a = 3;
    const b = 4;
    console.log(x, y); // 1, 2 (외부 함수의 변수에 접근 가능)
  }
  
  if (true) {
    let x = 5; // 새로운 블록 스코프에 x 선언
    const y = 6; // 새로운 블록 스코프에 y 선언
    let z = 7; // 블록 스코프에 z 선언
    console.log(x, y); // 5, 6
  }
  
  console.log(x, y); // 1, 2 (if 블록의 변수는 이 스코프에 영향 없음)
  // console.log(z); // ReferenceError: z is not defined
  
  innerFunc();
}

letConstScopeExample();

화살표 함수와 This 바인딩

일반 함수와 달리, 화살표 함수(arrow function)는 자신만의 this 바인딩을 생성하지 않는다.
대신, 화살표 함수는 정의된 함수 스코프의 this 값을 상속받는다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function ThisBindingExample() {
  this.value = 42;
  
  // 일반 함수 - 자신의 this 바인딩을 가짐
  this.regularMethod = function() {
    console.log(this.value); // undefined (메서드 호출 방식에 따라 다름)
  };
  
  // 화살표 함수 - 외부 스코프의 this를 사용
  this.arrowMethod = () => {
    console.log(this.value); // 42
  };
  
  // 비교 테스트
  setTimeout(this.regularMethod, 1000); // undefined
  setTimeout(this.arrowMethod, 1000);   // 42
}

const example = new ThisBindingExample();

이 차이는 setTimeout과 같은 비동기 함수나 이벤트 핸들러에서 특히 중요하다.

함수 스코프와 매개변수

함수의 매개변수(parameters)는 함수 스코프 내에서 지역 변수처럼 작동한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function parameterExample(a, b) {
  console.log(a, b); // 1, 2
  
  // 매개변수를 가리는 지역 변수 선언
  var a = 3;
  console.log(a, b); // 3, 2
  
  function innerFunction() {
    // 외부 함수의 매개변수와 변수에 접근 가능
    console.log(a, b); // 3, 2
  }
  
  innerFunction();
}

parameterExample(1, 2);

ES6의 기본 매개변수(default parameters)와 나머지 매개변수(rest parameters)도 함수 스코프에서 사용 가능하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function defaultParameterExample(a = 1, b = a + 1) {
  console.log(a, b); // 1, 2
}

defaultParameterExample();

function restParameterExample(a,...rest) {
  console.log(a);    // 1
  console.log(rest); // [2, 3, 4]
}

restParameterExample(1, 2, 3, 4);

함수 스코프와 Arguments 객체

모든 함수(화살표 함수 제외)는 arguments 객체를 자동으로 가지고 있다. 이 객체는 함수에 전달된 모든 인수에 접근할 수 있게 해준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function argumentsExample() {
  console.log(arguments); // Arguments 객체
  console.log(arguments[0]); // 첫 번째 인수
  console.log(arguments.length); // 인수의 개수
  
  // 배열로 변환
  const args = Array.from(arguments);
  args.forEach(arg => console.log(arg));
}

argumentsExample(1, "두번째", true);

arguments 객체는 함수 스코프 내에서만 존재하며, 내부 함수는 자신만의 arguments 객체를 가진다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function outerFunction(a, b) {
  console.log("외부 함수 arguments:", arguments); // [1, 2]
  
  function innerFunction(c, d) {
    console.log("내부 함수 arguments:", arguments); // [3, 4]
  }
  
  innerFunction(3, 4);
}

outerFunction(1, 2);

함수 스코프와 엄격 모드

엄격 모드(‘use strict’)는 함수 스코프에 영향을 미친다.
특히, 엄격 모드에서는 선언되지 않은 변수에 할당하면 오류가 발생한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function nonStrictFunction() {
  undeclaredVar = "선언 없이 할당"; // 문제 없음 (전역 객체에 속성 생성)
  console.log(undeclaredVar);
}

function strictFunction() {
  'use strict';
  // undeclaredVar = "선언 없이 할당"; // ReferenceError: undeclaredVar is not defined
}

nonStrictFunction();
strictFunction();

엄격 모드는 함수 단위로 적용할 수 있으며, 특정 함수에만 엄격 모드를 적용하려면 함수 본문 시작 부분에 ‘use strict’ 지시어를 추가한다.

함수 스코프의 실제 활용 예제

데이터 캡슐화와 은닉

 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
37
38
39
40
41
const userManager = (function() {
  // 프라이빗 데이터
  const users = [];
  let nextId = 1;
  
  // 프라이빗 함수
  function validateUser(user) {
    return user && user.name && typeof user.name === "string";
  }
  
  // 퍼블릭 API
  return {
    addUser: function(user) {
      if (validateUser(user)) {
        const newUser = { user, id: nextId++ };
        users.push(newUser);
        return newUser.id;
      }
      return null;
    },
    
    getUserById: function(id) {
      const user = users.find(user => user.id === id);
      if (user) {
        // 복사본 반환하여 원본 보호
        return { user };
      }
      return null;
    },
    
    getAllUsers: function() {
      // 복사본 배열 반환하여 원본 보호
      return users.map(user => ({ user }));
    }
  };
})();

// 사용 예
const userId = userManager.addUser({ name: "홍길동", age: 30 });
console.log(userManager.getUserById(userId)); // { name: "홍길동", age: 30, id: 1 }
console.log(userManager.getAllUsers()); // [{ name: "홍길동", age: 30, id: 1 }]

함수 팩토리

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function createMultiplier(multiplier) {
  // 외부 함수의 매개변수를 내부 함수가 기억
  return function(number) {
    return number * multiplier;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

이벤트 핸들러와 클로저

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function setupButtons() {
  const buttons = document.querySelectorAll('.button');
  
  buttons.forEach(function(button, index) {
    // 각 버튼에 대한 데이터 설정
    const buttonData = {
      id: `button-${index}`,
      clickCount: 0
    };
    
    // 이벤트 리스너 등록
    button.addEventListener('click', function() {
      // 클로저를 통해 buttonData에 계속 접근 가능
      buttonData.clickCount++;
      console.log(`${buttonData.id} 클릭 수: ${buttonData.clickCount}`);
    });
  });
}

모듈 패턴으로 상태 관리

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const counterModule = (function() {
  // 프라이빗 상태
  let count = 0;
  let lastOperation = null;
  
  // 프라이빗 함수
  function validateNumber(n) {
    return typeof n === "number" && !isNaN(n);
  }
  
  function updateLastOperation(operation, value) {
    lastOperation = {
      type: operation,
      value,
      timestamp: new Date()
    };
  }
  
  // 퍼블릭 API
  return {
    increment: function(n = 1) {
      if (validateNumber(n)) {
        count += n;
        updateLastOperation("increment", n);
      }
      return count;
    },
    
    decrement: function(n = 1) {
      if (validateNumber(n)) {
        count -= n;
        updateLastOperation("decrement", n);
      }
      return count;
    },
    
    getCount: function() {
      return count;
    },
    
    getLastOperation: function() {
      return lastOperation ? { lastOperation } : null;
    },
    
    reset: function() {
      count = 0;
      updateLastOperation("reset", 0);
      return count;
    }
  };
})();

console.log(counterModule.increment(5)); // 5
console.log(counterModule.decrement(2)); // 3
console.log(counterModule.getLastOperation()); // { type: "decrement", value: 2, timestamp: Date }

함수 스코프 관련 일반적인 문제와 해결 방법

전역 네임스페이스 오염

문제: 전역 변수와 함수가 너무 많아 이름 충돌이 발생할 수 있다.
해결: 네임스페이스 패턴이나 모듈 패턴을 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 네임스페이스 패턴
var MyApp = MyApp || {};

MyApp.utils = {
  formatDate: function(date) { /* 구현 */ },
  calculateAge: function(birthDate) { /* 구현 */ }
};

MyApp.models = {
  User: function(name) { /* 구현 */ }
};

// 사용
MyApp.utils.formatDate(new Date());

반복문에서의 클로저 문제

문제: var를 사용한 반복문에서 비동기 함수를 생성할 때 예상치 못한 동작이 발생한다.
해결: 즉시 실행 함수(IIFE)나 let을 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// IIFE 해결법
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index);
    }, 1000);
  })(i);
}

// let 해결법
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

This 컨텍스트 문제

문제: 함수 내에서 this가 예상과 다르게 바인딩된다.
해결: 화살표 함수, bind(), var self = this 패턴을 사용한다.

 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
function ThisProblemExample() {
  this.value = 42;
  
  // 문제
  setTimeout(function() {
    console.log(this.value); // undefined (this가 전역 객체를 가리킴)
  }, 1000);
  
  // 해결책 1: 화살표 함수 사용
  setTimeout(() => {
    console.log(this.value); // 42
  }, 1000);
  
  // 해결책 2: bind() 사용
  setTimeout(function() {
    console.log(this.value); // 42
  }.bind(this), 1000);
  
  // 해결책 3: self 패턴 사용
  var self = this;
  setTimeout(function() {
    console.log(self.value); // 42
  }, 1000);
}

new ThisProblemExample();

함수 스코프와 메모리 관리

함수 스코프는 메모리 관리에도 중요한 영향을 미친다.
함수가 종료되면 해당 함수 스코프의 변수들은 일반적으로 가비지 컬렉션의 대상이 된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function processLargeData() {
  // 대용량 데이터 생성
  const largeArray = new Array(10000000).fill('데이터');
  
  // 데이터 처리
  const result = largeArray.reduce((acc, item) => acc + item.length, 0);
  
  // 함수가 종료되면 largeArray는 가비지 컬렉션 대상이 됨
  return result;
}

const result = processLargeData();
// 이 시점에서 largeArray는 메모리에서 해제될 수 있음

하지만 클로저가 사용되면 해당 변수는 계속 유지된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function createDataManager() {
  // 대용량 데이터 생성
  const largeArray = new Array(1000000).fill('데이터');
  
  // 클로저가 largeArray를 참조하므로 메모리에서 해제되지 않음
  return {
    getItem: function(index) {
      return largeArray[index];
    },
    getLength: function() {
      return largeArray.length;
    }
  };
}

const manager = createDataManager();
// largeArray는 계속 메모리에 유지됨

메모리 누수 방지하기

클로저를 사용할 때 발생할 수 있는 메모리 누수를 방지하는 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function setupEventHandlers() {
  const button = document.getElementById('myButton');
  const largeData = new Array(1000000).fill('데이터');
  
  button.addEventListener('click', function() {
    console.log(largeData.length);
  });
  
  // 해결책: 함수 종료 시 불필요한 참조 제거
  return function cleanup() {
    button.removeEventListener('click', function() {
      console.log(largeData.length);
    });
    // 참조 제거
    largeData.length = 0;
  };
}

const cleanup = setupEventHandlers();
// 나중에 정리가 필요할 때
// cleanup();

ES6 모듈과 함수 스코프

ES6 모듈 시스템은 파일 기반 스코프를 제공하며, 함수 스코프와 함께 사용하면 더욱 강력한 모듈화가 가능하다.

1
2
3
4
5
6
7
8
9
// moduleA.js
const privateData = "private";

export function publicFunction() {
  console.log(privateData);
  return "public";
}

// privateData는 이 모듈 내에서만 접근 가능
1
2
3
4
5
// moduleB.js
import { publicFunction } from './moduleA.js';

console.log(publicFunction()); // "public"
// console.log(privateData); // ReferenceError: privateData is not defined

모듈 패턴과 ES6 모듈 비교

ES6 이전에는 IIFE와 클로저를 사용한 모듈 패턴이 널리 사용되었다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 전통적인 모듈 패턴
var MyModule = (function() {
  // 프라이빗 변수
  var privateVar = "private";
  
  // 프라이빗 함수
  function privateFunction() {
    return privateVar;
  }
  
  // 퍼블릭 API
  return {
    publicFunction: function() {
      return privateFunction();
    }
  };
})();

console.log(MyModule.publicFunction()); // "private"
// console.log(MyModule.privateVar); // undefined

ES6 모듈은 이런 패턴을 더 깔끔하게 대체할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// module.js
// 프라이빗 변수와 함수 (export하지 않으면 모듈 외부에서 접근 불가)
const privateVar = "private";

function privateFunction() {
  return privateVar;
}

// 퍼블릭 API (export한 것만 외부에서 접근 가능)
export function publicFunction() {
  return privateFunction();
}

함수 스코프와 재귀 함수

재귀 함수는 자기 자신을 호출하는 함수로, 함수 스코프와 밀접한 관련이 있다.

1
2
3
4
5
6
7
8
9
function factorial(n) {
  // 재귀 종료 조건
  if (n <= 1) return 1;
  
  // 자기 자신을 호출 (함수 스코프 내에서 자신을 참조)
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120

함수 표현식으로 정의된 재귀 함수는 함수 이름이 함수 스코프 내에만 존재한다:

1
2
3
4
5
6
7
8
9
const factorial = function innerFactorial(n) {
  if (n <= 1) return 1;
  
  // innerFactorial은 함수 내부에서만 접근 가능
  return n * innerFactorial(n - 1);
};

console.log(factorial(5)); // 120
// console.log(innerFactorial(5)); // ReferenceError: innerFactorial is not defined

함수 스코프와 디버깅 기법

함수 스코프 관련 문제를 디버깅하는 방법:

콘솔 로깅 기법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function scopeDebugging() {
  const outerVar = "외부 변수";
  
  function innerFunction() {
    const innerVar = "내부 변수";
    console.log({ scope: "innerFunction", outerVar, innerVar });
  }
  
  console.log({ scope: "scopeDebugging", outerVar });
  innerFunction();
}

scopeDebugging();

크롬 개발자 도구에서의 스코프 확인

크롬 개발자 도구의 Sources 패널에서 디버거를 사용하면:

  • Scope 패널에서 현재 함수 스코프의 모든 변수 확인 가능
  • Call Stack 패널에서 중첩된 함수 호출 스택 확인 가능
  • 중단점(breakpoint)을 설정하여 스코프 확인 가능

고급 함수 스코프 패턴

커링(Currying)

커링은 여러 개의 인수를 받는 함수를 하나의 인수만 받는 함수 여러 개로 분리하는 기법이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 일반 함수
function multiply(a, b) {
  return a * b;
}

// 커링된 함수
function curryMultiply(a) {
  // 내부 함수가 외부 함수의 매개변수에 접근
  return function(b) {
    return a * b;
  };
}

const multiplyBy2 = curryMultiply(2);
console.log(multiplyBy2(5)); // 10
console.log(multiplyBy2(10)); // 20

메모이제이션(Memoization)

메모이제이션은 함수의 결과를 캐싱하여 동일한 입력에 대해 계산을 반복하지 않는 최적화 기법이다.

 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
function createMemoizedFunction(fn) {
  // 클로저를 사용하여 캐시 저장
  const cache = {};
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (key in cache) {
      console.log('캐시에서 결과 반환');
      return cache[key];
    }
    
    console.log('계산 수행');
    const result = fn.apply(this, args);
    cache[key] = result;
    
    return result;
  };
}

const memoizedFactorial = createMemoizedFunction(function(n) {
  if (n <= 1) return 1;
  return n * this(n - 1);
});

console.log(memoizedFactorial(5)); // 계산 수행 (여러 번) + 결과
console.log(memoizedFactorial(5)); // 캐시에서 결과 반환 + 결과

함수 스코프 관련 모범 사례

  1. 전역 변수 최소화하기
  • 전역 변수는 충돌과 예상치 못한 동작의 원인이 될 수 있다.
  • 모듈 패턴, IIFE, ES6 모듈을 사용하여 전역 스코프 오염을 방지하는 것이 좋다.
  1. 즉시 실행 함수로 스코프 격리하기

    • 독립적인 기능을 IIFE로 감싸 스코프를 격리한다.
  2. 클로저 신중하게 사용하기

    • 클로저는 강력하지만 메모리 누수의 원인이 될 수 있다.
    • 필요할 때만 클로저를 사용하고, 사용이 끝나면 참조를 제거한다.
  3. var 대신 let과 const 사용하기

    • var는 함수 스코프를, letconst는 블록 스코프를 갖는다.
    • 블록 스코프가 더 예측 가능한 동작을 제공한다.
  4. 함수는 필요한 만큼만 중첩하기

    • 과도한 함수 중첩은 디버깅을 어렵게 만들 수 있다.
    • 일반적으로 3-4단계 이상의 중첩은 피하는 것이 좋다.
  5. 매개변수와 지역 변수 이름 충돌 피하기

    • 매개변수와 동일한 이름의 지역 변수를 선언하면 혼란을 야기할 수 있다.
  6. 함수 표현식에 이름 부여하기

    • 이름 있는 함수 표현식은 디버깅과 재귀에 유용하다.
  7. 모듈 패턴 활용하기

    • 관련 기능을 모듈로 그룹화하고 필요한 API만 노출한다.

참고 및 출처