타입스크립트에서의 클로저

타입스크립트에서의 클로저

이 글에서는 타입스크립트에서의 클로저에 대해 설명합니다.

YouTube Video

타입스크립트에서의 클로저

클로저란 무엇인가?

클로저란 함수가 정의된 스코프(환경)에 대한 참조를 저장할 수 있는 기능을 말하며, 정의된 스코프 외부에서 호출되더라도 그 환경에 접근할 수 있습니다. 아래에서는 타입 주석(type annotations)을 포함하여 클로저를 설명합니다.

간단히 말하자면, 클로저는 함수와 함수가 정의된 변수 환경을 결합하여, 함수 호출 시 그 환경에 접근할 수 있게 합니다.

클로저의 기본 작동 메커니즘

타입스크립트에서 함수가 다른 함수 내부에 정의될 경우, 내부 함수는 외부 함수의 변수에 접근할 수 있다는 점이 분명해집니다. 여기 타입 주석이 포함된 클로저의 기본 예제가 있습니다.

 1function outerFunction(): () => void {
 2    let outerVariable: string = "I am from outer function";
 3
 4    function innerFunction(): void {
 5        // The inner function accesses the variable of the outer function
 6        console.log(outerVariable);
 7    }
 8
 9    return innerFunction;
10}
11
12const closure: () => void = outerFunction();
13closure();  // "I am from outer function"
  • outerFunction의 반환 타입은 () => void로, 함수를 반환한다는 것을 나타냅니다.
  • innerFunction의 타입은 명시적으로 void로 설정되어 있으며, 반환값이 없음을 나타냅니다.

클로저의 용도와 이점

데이터 캡슐화

아래는 데이터를 캡슐화하기 위해 타입 주석이 포함된 클로저의 예제입니다.

 1function createCounter(): () => number {
 2    let count: number = 0;
 3
 4    return function (): number {
 5        count += 1;
 6        return count;
 7    };
 8}
 9
10const counter: () => number = createCounter();
11console.log(counter());  // 1
12console.log(counter());  // 2
13console.log(counter());  // 3
  • createCounter 함수는 () => number 타입의 함수를 반환합니다.
  • count 변수는 number 타입으로 정의되며, 클로저 내에서 조작됩니다.

고차 함수 (Higher-Order Functions)

클로저는 고차 함수를 생성할 때 유용합니다. 아래는 명확한 타입 주석이 포함된 고차 함수의 예제입니다.

 1function createMultiplier(multiplier: number): (value: number) => number {
 2    return function (value: number): number {
 3        return value * multiplier;
 4    };
 5}
 6
 7const double: (value: number) => number = createMultiplier(2);
 8console.log(double(5));  // 10
 9
10const triple: (value: number) => number = createMultiplier(3);
11console.log(triple(5));  // 15
  • createMultipliernumber 타입의 인수를 받고 (value: number) => number 타입의 함수를 반환합니다.
  • 내부 함수도 number 타입의 value를 받아 결과를 number 타입으로 반환합니다.

타입스크립트에서 클로저 구현 예제

타입 주석이 추가된 클로저로 범위 제한 카운터를 구현해 보세요.

 1function rangeCounter(min: number, max: number): () => number | string {
 2    let count: number = min;
 3
 4    return function (): number | string {
 5        if (count <= max) {
 6            return count++;
 7        } else {
 8            return `Count has exceeded the maximum value: ${max}`;
 9        }
10    };
11}
12
13const counter: () => number | string = rangeCounter(1, 5);
14
15console.log(counter());  // 1
16console.log(counter());  // 2
17console.log(counter());  // 3
18console.log(counter());  // 4
19console.log(counter());  // 5
20console.log(counter());  // "Count has exceeded the maximum value: 5"
  • rangeCounter 함수는 number 또는 string을 반환하는 함수를 반환합니다.
  • 내부 함수에서 countmax를 초과하면 string 타입 메시지를 반환하고, 그렇지 않으면 number 타입을 반환합니다.

클로저 사용 시 주의사항

클로저로 인한 잠재적 메모리 누수

클로저는 외부 범위의 변수를 유지할 수 있으며, 이는 때때로 메모리 누수를 유발할 수 있습니다. 불필요한 클로저는 명시적으로 메모리에서 해제해야 합니다.

 1function createLeak(): () => void {
 2    // Large array consuming significant memory
 3    const largeArray: string[] = new Array(1000000).fill("leak");
 4
 5    // Closure capturing `largeArray`
 6    return function (): void {
 7        console.log(largeArray[0]); // Using the captured array
 8    };
 9}
10
11// Create a closure that holds a reference to the large array
12let leakyFunction = createLeak();
13
14// The large array is not released as `leakyFunction` still references it
15
16// When the object is no longer needed
17leakyFunction = null; // Explicitly remove the reference
  • 이 코드에서 createLeak 내에서 생성된 largeArray는 스코프를 벗어나면 해제되어야 하지만, 클로저가 largeArray를 캡처하기 때문에 해제되지 않습니다. leakyFunction이 존재하는 한, 이 불필요한 메모리는 계속 유지됩니다.
  • 객체나 변수가 더 이상 필요하지 않을 경우 참조를 null로 설정하면 가비지 컬렉터가 이를 감지하고 메모리를 해제할 수 있습니다.

루프에서 클로저의 오용

루프 내에서 클로저를 생성할 때 동일한 변수를 참조하는 문제의 가능성이 있습니다. 다음 예제는 변수가 i가 올바르게 작동하지 않는 경우를 보여줍니다.

1for (var i: number = 0; i < 3; i++) {
2    setTimeout((): void => {
3        console.log(i);
4    }, 1000);
5}
6// Output: 3, 3, 3

이 코드는 루프가 끝날 때 i가 값 3을 참조하므로 원하는 결과를 도출하지 않습니다. 이를 해결하려면 let을 사용하여 스코프를 분리하거나 즉시 호출 함수(IIFE)를 사용하세요.

1for (let i: number = 0; i < 3; i++) {
2    setTimeout((): void => {
3        console.log(i);
4    }, 1000);
5}
6// Output: 0, 1, 2

let을 사용하면 각 루프 반복마다 i의 스코프가 분리되어 기대한 결과를 얻을 수 있습니다.

요약

TypeScript에서는 클로저를 사용하여 타입 시스템을 활용함으로써 더 안전하고 예측 가능한 코드를 작성할 수 있습니다. 클로저를 올바르게 사용하면 데이터 캡슐화 및 고차 함수의 유연한 설계를 할 수 있습니다. 추가적으로, 클로저를 사용할 때 메모리 관리 및 의도치 않은 스코프 참조에 주의해야 합니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video