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
|
위 예시에서 functionScopedVar
는 exampleFunction
내부에서만 존재하며, 함수 외부에서는 접근할 수 없다.
함수 스코프와 전역 스코프의 관계#
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에서 도입된 let
과 const
는 블록 스코프를 가진다.
이들은 함수 스코프보다 더 제한적인 스코프를 제공한다.
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
의 실행이 종료된 후에도 해당 변수는 클로저를 통해 유지된다.
클로저는 다음과 같은 상황에서 유용하다:
- 데이터 캡슐화와 정보 은닉
- 이벤트 핸들러와 콜백 함수
- 팩토리 함수와 모듈 패턴
- 커링(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)); // 캐시에서 결과 반환 + 결과
|
함수 스코프 관련 모범 사례#
- 전역 변수 최소화하기
- 전역 변수는 충돌과 예상치 못한 동작의 원인이 될 수 있다.
- 모듈 패턴, IIFE, ES6 모듈을 사용하여 전역 스코프 오염을 방지하는 것이 좋다.
즉시 실행 함수로 스코프 격리하기
- 독립적인 기능을 IIFE로 감싸 스코프를 격리한다.
클로저 신중하게 사용하기
- 클로저는 강력하지만 메모리 누수의 원인이 될 수 있다.
- 필요할 때만 클로저를 사용하고, 사용이 끝나면 참조를 제거한다.
var 대신 let과 const 사용하기
var
는 함수 스코프를, let
과 const
는 블록 스코프를 갖는다.- 블록 스코프가 더 예측 가능한 동작을 제공한다.
함수는 필요한 만큼만 중첩하기
- 과도한 함수 중첩은 디버깅을 어렵게 만들 수 있다.
- 일반적으로 3-4단계 이상의 중첩은 피하는 것이 좋다.
매개변수와 지역 변수 이름 충돌 피하기
- 매개변수와 동일한 이름의 지역 변수를 선언하면 혼란을 야기할 수 있다.
함수 표현식에 이름 부여하기
- 이름 있는 함수 표현식은 디버깅과 재귀에 유용하다.
모듈 패턴 활용하기
- 관련 기능을 모듈로 그룹화하고 필요한 API만 노출한다.
참고 및 출처#