11. 클로저 (Closure)

Closure 란 ?

클로저란 내부 함수가 외부로 반환된 이후에도 life-cycle 이 유지되는 것을 말합니다. 클로저 안에 정의된 함수는 만들어진 환경을 기억합니다.

Lexical Scope

제가 쓴 글을 한번 읽어보셔도 도움이 될 것 같아요. (미디엄)[https://medium.com/@appear.ko/%EB%A0%89%EC%8B%9C%EC%BB%AC-%EC%8A%A4%EC%BD%94%ED%94%84-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-ef3c8e8584d4]

var name = "olaf"; // 전역에 name 이라는 변수를 하나 선언했습니다.

function print() {
  console.log(name);
}

function getName() {
  var name = "innerName";
  print(); // olaf
}

getName();

위에 콘솔을 찍어주는 print() 함수를 호출합니다. 결과는 olaf 가 찍힙니다. 왜일까요? 저는 처음에 당연히 getName 함수안에서 실행이 되었으니 getName 의 name 을 찍을줄 알았습니다. 이 때 나오는 개념이 렉시컬 스코프입니다. print 라는 함수는 getName의 밖에 선언 되어 있습니다. 글로벌 영역이지요 print() 가 실행이 된다면 자신의 로컬 영역에서 name을 한번 찾을 테고 없으니까 자신이랑 가까운 글로벌 영역으로 올라가 글로벌의 name를 찍는 것 입니다. 쉽게 생각했을때 이처럼 소스코드가 작정되는 시점에 이미 스코프가 정해지는 것을 렉시컬 스코프라고합니다.

  • outer 는 inner 라는 함수를 리턴한다.

  • inner 는 생성 당시의 스코프 체인을 기억하고있다.

  • foo 라는 변수에 outer 의 return 값인 inner (내부에서 외부를 참조하는 스코프 체인을 기억하고있는)를 담았다.

  • foo() 를 실행한다. 이는 inner() 를 실행하는 것과 같다. inner 는 스코프체인을 기억하고 있기 때문에 outer 의 num 까지 접근이 가능하다. 결과적으로 num 에는 10 이 출력된다.

언제 클로저를 이용할까 ?

  • private 접근제어: Javascript 에는 일반적으로 접근 제어자라는 값이 없다. private 하게 값의 접근을 제어하고 싶다면 스코프를 이용 할 수 있는데, 이 때 클로저를 사용한다.

클로저를 이용하면 아래와 같이 count 변수를 숨기며 값을 제어 할 수 있도록 만들 수 있다.

  • 함수형 프로그래밍: 사이드이펙트를 일으키지 않는 목적을 가진 함수들의 조합

add 라는 함수는 a 라는 인자를 받아 새로운 b 라는 인자와 더해주는 함수(기존의 a 를 참조한 상태로 리턴된다)를 리턴한다. 우리는 add 라는 함수를 이용하여 어떤 값이 들어오더라도 10 을 더해주는 add10 이라는 함수를 만들 수 있다.

  • 루프 내부의 클로저: 클로저가 가진 스코프 문제

굉장히 유명한 단골 문제라고 생각한다. for 루프를 돌면서 setTimeout 이 돈다. 당연히 해당 setTimeout callback 은 0 의 time 을 가지기 때문에 해당번째의 index 가 찍힐 것 이라는 기대를 가지게한다.

하지만 기대했던 결과와는 다르게 i 에는 6 이라는 값이 찍히게되는데, 쉽게 설명하자면 setTimeout 이 동작하는곳과 for 문이 동작하는 곳이다르다. for 문이 먼저 동작하고 for 의 동작이 끝나면 setTimeout 이 호출된다. 이때는 이미 i 가 6이된 상태이기 때문에 console 에는 6이 출력되는 것이다.

이 문제를 어떻게 해결 할 수 있을까?

가장 간단한 방법은 즉시 실행함수를 이용해 index별로 함수를 실행하여 스코프를 갈라버리는 것이다.

또는 let 을 이용하는 방법이 있다.

let 은 for 문에서 특이하게 동작하는데, 해당 코드가 babel 을 통과하여 transpile 이 된다면, setTimeout 부분을 별도의 함수로 분리하여 아래와 같이 동작이 되도록 변경한다.

️🌸 Thi

예제

memorize

12. HttpRequest 를 먼저 보고 다시 돌아오시는 것을 추천드립니다 :)

function memoize(fn) {
  const cache = {};
  return async url => {
    if (cache[url]) {
      console.log(`cached: ${url}`);
      return cache[url];
    } else {
      const data = await fn(url);
      cache[url] = data;
    }
  };
}

async function http(url) {
  const res = await fetch(url);
  const data = await res.json();
  return data;
}

async function init() {
  const memoizeFetch = memoize(http);

  Promise.all([
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/1/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/2/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/3/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/4/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/5/")
  ]);

  Promise.all([
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/1/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/2/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/3/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/4/"),
    await memoizeFetch("https://pokeapi.co/api/v2/pokemon/5/")
  ]);
}

init();

함수형 프로그래밍

function add(a){
  return function(b) {
    return a + b
  }
}

var add10 = add(10)
var result = add10(10) // 20

커링

function foo(a) {
  return function(b){
    return function(c) {
        console.log(a + b + c)
    }
  }
}
foo(10)(20)(30)

클로저에 대한 개념을 여기서 더 다루기 보다는 언제 어떻게 쓰이는지를 다루면서 좀 더 이야기를 나눠보는 것이 이해해 빠를거라 생각하여 간단한 예제만 추가하였습니다.

뒤에서 알아볼 module pattern 들을 살펴보면서 왜 클로저를 사용해야하며 무엇이 좋은지를 살펴보겠습니다.

Last updated