JS Reboot - 실행 컨텍스트와 클로저

코스·2020년 9월 13일
2

JS Reboot

목록 보기
2/4
post-thumbnail

최근 글쓴이는 우아한테크러닝 3기 교육을 받고있습니다. 이전에 사용했던 Javascript 스펙들을 다시보게 되면서 배운 점들을 시리즈 글로 올려볼까 합니다. 글에 오류가 있거나 궁금한 점이 있다면 언제든 댓글로 알려주세요!

실행 컨텍스트

이전 글에서도 this의 사용법이 다양하다고 언급했습니다. 대체로, this는 호출 당시의 상황을 보고 this가 가리키는 대상을 정하게 됩니다. 이렇게 상황에 따라 달라지는 this의 결정 방식을 실행 컨텍스트라고 합니다.

const person = {
  name: '박성우',
  getName() {
    return this.name;
  }
}

console.log(person.getName());

위 예시에서, person 객체는 getName이란 함수를 가지고 있는데, 이때 반환값으로 this를 사용하게 됩니다. Java와 같이 객체지향에 익숙한 사람은 '이때 this는 당연히 person 본인을 말하는 거겠지?' 라고 생각하며 코드를 이해하고, 실제로 출력되는 값도 생각한 결과와 동일하게 나오게 됩니다.

const man = person.getName;

console.log(man());

다만, 다음과 같이 man으로 호출하게 된다면 예상했던 name값이 출력되지 않습니다. 위에서 언급했듯this는 현재 호출시 상황을 보고 대상을 정하는데, 첫번째 예시의 경우 thisperson.getName()으로 호출되었기 때문에 personthis의 대상으로 정한 것입니다. 그러나 두번째 예시는 그저 man()으로 호출되었음으로, this는 가리킬 대상을 찾지 못해 결국 기본적인 사용방식인 전역객체(브라우저에서는 window 객체)를 가리키게 됩니다.

🤔 man을 선언할 때 person.getName으로 값을 지정했기 때문에 그때부터 this가 결정될 것 같다고 생각할 수 있지만, 이전 글에서도 말했듯 현제 man은 변수이고 함수도 값이기 때문에 함수가 호출되지 않고 값으로 저장되었음으로 그 상황에서는 this가 호출되지 않은 상태입니다.

그런데, 저런 상황이 실제로 많이 일어날까요? 코딩에서 많이 사용하지 않은 기법이라고 생각하겠지만 저희는 은근 저런 상황을 많이 마주치게 됩니다. 이벤트 시스템을 예로 들어볼까요? 만약 버튼을 클릭할 때 실행할 함수를 만들어야 한다면, 함수를 명시하기 위해 콜백함수를 사용하게 됩니다. 콜백함수에서 this를 사용하게 된다면? 그때는 실제 콜백함수가 호출하는 환경에 따라 this가 가리키는 대상이 바뀌게됩니다.

이렇게 this의 대상이 계속 바뀌게 되는 것을 방지하기 위해, Javascript 스펙에 이를 고정하는 함수를 제공하는데, 그중 한 함수가 bind 함수입니다.

button.addEventListener('click', person.getName.bind(person));

다음과 같이 적게 된다면, person.getName이 실행될 때 마다 항상 thisperson으로 해석하라고 지정할 수 있습니다. 이 외에도 call, apply등을 사용해 this를 지정할 수 있습니다.

화살표 함수와 this

화살표 함수(Arrow function)는 ES6에서부터 추가된 스펙입니다. 기존 함수에 비해 더욱 간단하게 함수를 표현할 수 있는 특징이 있습니다.

const foo = function(x) {

};

const bar = (x) => {

};

위와 같이 foobar는 같은 기능을 하는 함수입니다.

실행 컨텍스트를 이야기하다 갑자기 화살표 함수를 이야기하는 이유는, 화살표 함수 안에서 this는 항상 상위 scope의 this를 가리킵니다. 이를 Lexical this라고 하는데, 이 스펙을 활용한다면 위에서 사용한 person 객체를 다음과 같이 수정할 수 있습니다.

const person = {
  name: '박성우',
  getName: () => {
    return this.name;
  }
}

console.log(person.getName()); // 박성우

const man = person.getName;

console.log(man()); // 박성우

두 방식 다 thisperson을 가리키도록 고정되었기 때문에 의도대로 정상 작동합니다.

💡 이전 React에서 클래스 컴포넌트에서 이벤트 콜백함수를 추가할 때
this.onClickButton = this.onClickButton.bind(this);와 같이 처리하는 것도 실행 컨텍스트와 관련이 있습니다. 다만 이는 화살표 함수로 선언한다면 위와 같이 처리할 필요가 없습니다.

클로저

function foo(x) {
  return function bar() {
    return x;
  }
}

const f = foo(10);

console.log(f()); // 10

위 콜백함수는 bar에서 foo의 매개변수인 x를 다루게 되는데, 일반적인 프로그래밍 언어에서는 이해가 안되는 로직입니다. f라는 변수를 만들 때 변수 xfoo스코프가 끝나면 소멸될탠데, 이후 f변수를 사용할 때 x가 살아남아 리턴된다는 것은 신기한 일 일것입니다. 이는 콜백함수로 함수를 리턴할 때 반환된 함수가 사용하는 외부 스코프를 저장하는데, 이를 클로저라고 합니다.

클로저를 적절하게 사용할 수 있는 예시는 객체 안 값을 보호할 때 사용할 수 있습니다.

const person = {
  age: 10,
}

person.age = 500;

만약 다음과 같이 person 객체를 만들었다고 하면, 그 안 age는 정말 마음대로 바꿀 수 있습니다. 코드를 짠 의도처럼 실제 나이를 의미하는 값이라고 하면 터구니없는 500이라는 값을 넣는다고 해도 문제가 되지 않는다는 것입니다. 그러면 어떻게 해서 객체의 값을 제한할 수 있을까요?

function makePerson() {
  let age = 10;
  return {
    getAge() {
      return age;
    },
    setAge(x) {
      age = x > 1 && x < 130 ? x : age;
    }
  }
}

const p = makePerson()

console.log(p); // {getAge: (fun), setAge: (fun)}

p.setAge(150);
console.log(p.getAge()); // 10

p.setAge(30);
console.log(p.getAge()); // 30

위와 같이 객체를 만드는 함수 makePerson을 만든다면, age 값을 보호할 수 있습니다. 객체의 getAge를 통해 age값을 가져올 수 있고, setAge를 통해 age값을 설정할 수 있습니다. 특히나, setAge의 경우에는 받은 값이 특정 범위를 넘어가면 받은 값을 age로 설정하지 않고 기존 값을 유지하는 기능을 넣을 수 있습니다. 그리고 클로저 덕분에 setAgeage 값을 수정해도 age 값을 같이 공유하기 때문에 getAge를 했을 때 같은 age 값을 가져올 수 있는 것입니다.

💡 클로저를 보면, 함수형 프로그래밍의 커링(currying)을 생각하게 되는데 두 개념은 서로 같은 개념으로 생각할 수 있습니다. 클로저라는 개념을 활용한 것이 커링이고, 커링을 하기 위한 수단으로 클로저를 사용한것이죠.

다음에는 Modern Javascript의 꽃인 비동기에 대해 다뤄보겠습니다.

참고문서

profile
잡다한거 하는 개발자

0개의 댓글