코어자바스크립트 - this

2021-08-162021-08-16
  • Javascript
  • Book

📖 이 글은 코어자바스크립트를 읽고 책을 바탕으로 이해한 내용을 작성한 글입니다.

this meme

자바스크립트를 처음 공부할 때부터 혼란에 빠지게 했던 this. 위안이 되는 것은, 위의 짤에서도 알 수 있듯 this에 혼란스러워 하는 사람이 나 뿐만이 아니라는 것이다 😂

코어자바스크립트 3장에서는 this에 대한 내용을 딥하게 톺아보는데, 3장을 읽고 해소할 수 있었던 궁금증은 다음과 같다.

  • this란 대체 정확히 무엇인가
  • 어느 상황에서 무슨 이유로 this는 @@이 되는가?
  • 화살표 함수의 특징과 활용
  • apply, call, bind 메소드의 차이점과 활용
  • 함수와 메소드의 차이점

일단 this가 뭔지부터 알아보자 🤔

호출한 주체에 대한 정보가 담긴 것
호출한 주체를 가리키는 것
바라보는 대상

자바스크립트에서 this는 어디서든 사용가능하고, 상황에 따라 바라보는 대상이 달라진다. 하지만 다른 언어에서는 대체적으로 클래스로 생성한 인스턴스에서만 사용가능하다고 한다. 이게 자바스크립트는 weird하다고 많은 개발자가 외치는 이유 중 하나이지 않을까 싶다.

하지만 그렇기에 자바스크립트에서 this는 함수와 객체 메소드를 실질적으로 구분시켜줄 수 있는 거의 유일한 기능이기도 하다. this의 의미 자체는 위와 같이 이야기할 수 있을 것 같은데, **‘호출한 주체’**라는 것은 또 상황에 따라 달라질 수 있다.

왜냐하면 this는 앞서 2장에서 살펴보았던 실행 컨텍스트가 생성될 때 함께 결정되기 때문이다. 그리고 공부했던 내용대로라면 실행 컨텍스트는 함수가 호출되어 콜 스택에 담길 때 생성된다. 따라서, 함수가 호출될 때 this가 결정된다고도 말할 수 있겠다.

전역 공간에서의 this와 실행 컨텍스트

그리고 우리는 전역공간에서의 실행 컨텍스트는 자동으로 생성된다고 2장에서 보고 왔다. 따라서, 전역공간에서의 this도 자동으로 생성되며 전역 객체를 가리키게 된다. 여기서 전역 객체는 window 혹은 node환경이라면 global을 의미한다.

this를 이해하기 위한 핵심 개념 중 하나가 프로퍼티인데, 프로퍼티에 대해 좀 더 자세히 알기 위해서는 자바스크립트 변수 동작 방식에 대해서 알아야 한다. 자바스크립트의 모든 변수는, 특정 객체 (Lexical Environment)의 프로퍼티로서 동작한다. 왜냐? 실행컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장하니까. 즉, 자바스크립트 엔진이 전역변수를 전역객체의 프로퍼티로 자동 할당하고 있다는 이야기이다.

이 말은 곧 다음과 같다

  • window 전역 객체는 this로 대체할 수 있다

    window.a = 'hello';
    console.log(a); // 'hello'
    
    this.a = 'hello';
    console.log(a); // 'hello'
  • 전역변수를 선언하는 것과 window 객체에 프로퍼티로 직접 할당하는 것은 동일하다 (단, 삭제는 다르다)

    window.a = 'hello';
    console.log(a); // 'hello'
    
    const a = 'hello';
    console.log(a); // 'hello'

직접 할당하는 것은 동일하나 삭제하는 것은 다른 이유는, 변수를 선언하면 자바스크립트 엔진이 전역객체의 프로퍼티로 할당하는데 이 때 자동으로 configurable 속성을 false로 정의하기 때문이다. 이는 개발자의 실수를 방지하기 위함이라고 한다. 따라서 직접 할당한 경우는 delete를 이용해서 삭제가 가능하지만 변수로 선언한 경우에는 삭제가 불가능하다.

그리고 window.locationlocation 이라고만 해도 접근할 수 있는 이유는 그게 전역객체의 프로퍼티이기 때문에, 스코프 체인에서 location을 검색하다가 가장 마지막에 도달하는 전역객체에서 발견하고 그 값을 반환하기 때문이다.

이러한 프로퍼티와 관련된 내용에 대해 이해하는 것은 this를 결정짓는 호출 주체와 관련되어 있다. 함수와 메소드의 차이점에 대해 살펴보기 위한 일종의 빌드업?

함수와 메소드의 차이점과 this

메소드

함수와 메소드는 동일한 것처럼 보이기 때문에 처음 자바스크립트를 공부할 때 혼란의 원인이 되었다. 물론 메소드는 어떤 객체에 속한 함수이다, 라고 정리하면서 혼란이 해소되는 듯했지만…

사실 엄밀하게 말하면 반만 맞는 설명이라고 한다. 객체에 속한 함수라고 해서 전부 메소드인 것이 아니라, 객체에 속한 함수로서 호출해야 메소드이기 때문이다.

또한 함수와 메소드는 독립성 측면에서 차이점을 갖는다. 함수는 그 자체만으로도 쓰일 수 있는 독립적인 기능이지만 메소드는 자신을 호출한 대상 객체에 한해서만 동작을 수행한다. 때문에 this를 갖고 있다.

함수와 메소드를 눈으로 구별하여 알아보기엔 매우 쉬운데, 닷 노테이션이나 대괄호 표기법을 보면 되기 때문이다. obj.method()처럼 닷 노테이션이나, obj['method_1']처럼 대괄호 표기법을 사용하면 그 앞의 객체에 대한 함수로서 호출된 것이므로 메소드이다. 그리고 그 객체가 호출한 주체이므로 this가 된다.

함수

반면 함수는 어떨까? 메소드는 this를 갖게 되지만 함수는 this를 갖지 않는다. 정확하게 말하자면 this에 무언가가 할당되지 않는다. 따라서 함수는 this를 콘솔에 찍어보면 전역객체가 나온다.

왜냐면 호출 주체를 따로 명시하지 않고, 개발자가 코드에 직접 관여하여 실행한 것이기 때문이다. 닷 노테이션이나 대괄호 표기법으로 구별할 수 있다는 게 바로 이런 의미이다.

결론적으로 this를 결정하는 것에 대해서는 함수 실행 당시의 주변 환경은 중요하지 않다. 오로지 함수 호출 구문 앞에 호출 객체가 명시가 되었느냐가 관건이다.

그러나 이런 this의 특징은 ‘그래서 뭔데?’ 하는 혼란을 불러일으키기 때문에, 호출 주체가 없을 경우 this가 전역 객체가 아니라 호출 당시의 주변 환경의 this를 상속받게 하고 싶다는 니즈가 생겨났다. 그렇게만 할 수 있다면 흐름이 훨씬 자연스럽고, 스코프 체인상의 일관성을 유지할 수 있기 때문이다. 스코프 체인으로 변수를 찾는 것처럼, 내부에서 this를 찾고 없으면 상위 컨텍스트에서 찾고. 이런 일관성 말이다.

그럼 그렇게 할 수 있게 하기 위해서 그동안 어떤 방법들이 고안되었을까?

this를 컨트롤하기 위한 노력들

코어자바스크립트에서는 상위 컨텍스트의 this를 이용하거나 원하는 값으로 this를 바인딩하는 등 어디로 튈지 모르는 this를 컨트롤하기 위한 방법들을 소개한다.

변수에 this를 저장

es5까지의 환경에서는 this를 상속할 방법이 없어 이런 방법을 사용했다고 한다. 아주 간단한데, 변수에 상위 스코프의 this를 저장하고 내부 함수에서 활용하는 것이다.

변수명 컨벤션은 _this, _, that, self 등이었다고 하는데 self를 가장 많이 썼다고 한다. 물론 지금은 이렇게 안 하고 다른 방법을 사용하기 때문에 낯설지만, 레거시 코드를 언젠간 꼭 볼 일이 있는만큼 추후 저런 낯선 변수명을 보더라도 당황하지 않을 수 있을 것 같다.

화살표 함수

const func = () => {}
es6에서 도입된 화살표 함수는 this의 고민을 해결해준다.

  • this를 자동으로 바인딩하지 않음
    따라서 스코프 체인에 의해 가장 가까운 상위 스코프의 this를 찾아 활용한다. 때문에 리액트의 클래스형 컴포넌트에서 메소드 선언시 화살표 함수를 이용한 것이다.

  • 화살표 함수와 익명함수는 다르다
    화살표 함수가 무조건 익명함수의 모습을 할 수 밖에 없는 것 뿐, 익명함수가 화살표 함수인 것은 아니다.

call

Array.prototype.push.call()

  • 대상 함수를 즉시 실행한다
  • 첫 번째 인자를 대상 함수의 this로 바인딩한다.
  • 이후 두 번째 인자부터는 대상 함수의 매개변수로 전달되며 ,로 구분된다.
  • 함수, 메소드 구분 없이 적용된다.

apply

Array.prototype.push.apply()

  • call과 동일하게 동작한다
  • 차이점은, 매개변수를 전달할 때 두 번째 인자에 배열로 전달한다는 것이다.
  • 함수, 메소드 구분 없이 적용된다.

bind

func.bind()

  • call과 비슷하지만 (apply처럼 배열로 매개변수를 전달하지 않고 call의 방식과 같다) 함수를 즉시 실행하지 않는다.

  • this 바인딩과 매개변수를 넘겨받은 새로운 함수를 반환한다

  • 반환된 함수를 호출할 때 전달된 매개변수들은, bind 메소드로 전달된 매개변수 뒷순위이다.

  • 이를 이용해 부분 적용 함수를 구현할 수 있다.

    const func = function (a, b, c, d) {
      console.log(this, a, b, c, d);
    };
    func(1, 2, 3, 4); // window, 1, 2, 3, 4
    const test = func.bind({ x: 1 }, 4, 5);
    test(6, 7); // 부분 적용 ! {x:1}, 4, 5, 6, 7
  • bind만의 특이점이 하나 더 있는데, name 프로퍼티에 bound 접두어가 붙는다는 것이다. 이로써 apply, call은 코드를 추적하기 어렵게 만든다는 단점을 bind는 해결할 수 있다. 해당 접두어를 보고 bind를 적용하였음을 바로 알 수 있기 때문이다.

apply,call을 활용할 수 있는 케이스

코어자바스크립트에서는 apply와 call 메소드를 유용하게 활용할 수 있는 사례들을 소개한다. 물론 이들은 명시적으로 별도의 this를 바인딩하기 때문에 this를 예측하기 어렵게 만든다는 단점이 있긴 하지만 장점들이 단점을 이겼는지 ES5이하 환경에서는 실무에서 아주 유용하게 활용되었다고 한다.

아마 지금 자바스크립트를 공부하는 대다수의 사람들은 ES6이후의 환경에 익숙할 것 같은데, ES5 이하 환경에서의 코드를 아예 안 보고 살 수는 없는 것이므로 미리 익혀두고 익숙해지면 좋겠다는 생각을 했다.

유사배열객체에서 배열 메서드 사용하기

원칙적으로 객체에는 배열 메서드를 직접 사용할 수 없으나, call, apply등을 이용하면 가능하다. 유사배열객체라는 것은 말 그대로 배열처럼 생긴 객체라는 것이다. 종류로는 nodelist, arguments등이 있다.

const obj = {
  0: '과일',
  1: '사과',
  2: '포도',
  3: '배',
};

이런 객체가 있을 때 key가 배열의 index넘버이고 value가 배열의 요소라고 생각될 수 있어 배열과 유사한 객체라고 말한다. 이중에서도 key가 0 또는 양의 정수인 프로퍼티이고 length 프로퍼티가 0 또는 양의 정수인 객체는 call이나 apply를 이용해 배열 메소드를 이용할 수 있다.

예시로, Array.prototype.push.call(obj, 'd')라고 하면 '4':'d'가 추가된다. 하지만 모든 배열 메소드를 완벽하게 사용할 수 있는 것은 아니다. concat같은 메소드는 대상이 반드시 배열이어야 하기 때문에 사용했을 때 정확하지 않은 결과를 보여줄 수 있다. 그리고 splice같은 원본 문자열에 변경을 가하는 메소드는 에러를 뿜을 수 있는데, 유사배열객체의 length가 읽기 전용이기 때문이다.

이런 단점들 때문에 현재는 Array.from을 이용해 배열로 바꾸고 배열 메소드를 이용한다.

유사배열객체를 배열로 변환하기

call, apply, slice를 이용해 유사배열객체를 배열 형태로 복사할 수 있다.

const arr = Array.prototype.slice.call(obj);
console.log(arr); // ['과일', '사과', '포도', '배', 'd']

이게 가능한 이유는 slice 메소드가 시작 인덱스 넘버와 끝 인덱스 넘버를 받아 그 사이의 배열 요소를 추출하는데, 아무런 인자를 넘기지 않아 시작/끝 인덱스 넘버가 주어지지 않았기 때문에 원본 배열의 얕은 복사본을 반환하기 때문이다. 결국 유사배열객체의 얕은 복사를 수행한 건데 slice가 배열 메소드이기 때문에 복사본이 배열의 형태를 띄는 것이라고 한다.

생성자 내부에서 다른 생성자를 호출하기

만약 B라는 생성자 내부에서 선언할 코드가 이미 A라는 생성자에서 선언되어있다면 같은 코드를 반복하지 않고 A 생성자를 B 생성자 안에서 호출함으로써 반복과 중복을 줄일 수 있다. 이때 this가 B 인스턴스여야 하므로 call이나 apply를 이용해 this를 바인딩한다.

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때

간혹 함수의 매개변수로 넘겨야 할 인수들이 너무 많아 하나의 배열로 묶어 전달하고 싶을 때가 있다. 이럴 때는 apply를 쓸 수 있는데 가장 대표적인 예시로, Math.max()가 있다. apply를 이용하여 매개변수만 배열로 전달하고 싶을 때, this 자리는 null로 전달하면 된다.

const x = [1, 2, 3, 4, 5];
Math.max.apply(null, x); // 5

위와 같은 예시는 디바운스 함수를 구현하여 반환하는 함수에 이벤트 인자들을 배열로 한꺼번에 넘겨줄 때에도 쓰여, 꽤 활용도가 높다. 근데 물론 ES6에서는 아래와 같이 spread 연산자를 이용하면 더 간단하게 쓸 수 있다 😇

const x = [1, 2, 3, 4, 5];
Math.max.(...x); // 5

그 외 특수한 상황들에서의 this

콜백 함수에서의 this

우선 콜백함수의 정의에 대해서부터 알아보자.

코어자바스크립트에서는 콜백함수란 제어권을 다른 함수에게 넘겨준 함수라고 말하고 있다. 정확하게는 함수 A의 제어권을 다른 함수(메소드 포함) B에게 넘겨주었을 때 이 A함수를 콜백함수라고 한다는 것이다. 때문에 A함수는 B함수의 내부 로직에 따라 실행되며 this도 B함수 내부 로직에 의해 결정된다. 만약 B함수에서 A함수의 this를 별도 지정한다면 그게 A함수의 this가 된다.

이러면 결국 B함수에서 어떻게 하냐에 따라 다 다를 것이므로, 무조건 이거다! 라고 말할 수 없다. 심지어 콜백함수에 따라, 별도의 인자로 this를 지정할 수 있게 하는 경우도 있다. 이런 경우는 주로 배열 메소드에 많은데 리스트는 아래와 같다. 이런 이유들로 콜백함수에선 this가 무엇일지 예측하기 힘들다.

콜백 함수 인자로 this를 지정할 수 있는 배열 메소드

forEach, map, filter, some, every, find, findIndex, flatMap, from

책에서 소개하는 콜백 함수의 예시는 다음과 같다.

const arr = [1, 2, 3, 4];
arr.forEach((num) => console.log(this, num)); // this는 window

forEach라는 배열 메소드의 콜백 함수에서는 this를 따로 지정해주고 있지 않으므로 전역객체를 가리킨다.

//html
<button class='button'></button>;

//js
const btn = document.querySelector('.button');
btn.addEventListener('click', (e) => {
  console.log(e, this); // this는 button 태그
});

addEventListener메소드는 콜백 호출시 자신의 this를 상속하도록 정의되어있다. 따라서 닷 노테이션 앞의 객체가 this가 되므로 버튼 태그가 this로 콘솔에 찍히는 것이다.

생성자 함수에서의 this

우선 생성자 함수라는 것은 new라는 생성자 키워드와 함께 함수를 호출하는 것이다. 이러면 함수는 생성자로서 동작하고, 공통된 성질을 지니는 객체들을 생성하는데 사용한다. 생성자 함수 내부의 this는 인스턴스를 의미하는데, 생성자 함수에 의해 생성된 인스턴스에서도 this는 그 인스턴스를 가리킨다.

생성자 함수를 호출하면 일어나는 일

  1. 생성자의 prototype 프로퍼티를 참조하는 객체 (인스턴스)를 만든다. 이 객체는 proto라는 프로퍼티를 가진다.
  2. 미리 준비된 공통 속성과 개성을 해당 객체에 부여한다
  3. this는 그 해당 객체를 가리킨다.
const test = new People();

결론

특히나 콜백함수에서는 this를 예측하기 힘드므로 평소 this가 무엇일지 예측하는 연습을 해야한다.
this는 주변환경이 아니라 호출하는 주체가 있냐 없냐에 영향을 받아 결정된다.
코어자바스크립트 3장을 읽고 다음과 같은 질문들에 대답할 수 있다.

  • this란 무엇인가?
    자바스크립트에서 호출한 주체를 가리키며, 함수 실행 당시 함수 호출 주체가 있었냐 없었냐 (닷 노테이션, 대괄호 표기법)에 따라 달라진다.

  • call, apply, bind의 차이점은?
    call과 apply는 둘 다 함수를 호출하면서 this를 바인딩하지만, 그 대상이 되는 함수에게 매개변수를 전달하는 방법이 다르다. call은 두 번째 인자부터 전부를 매개변수로 전달하지만 apply는 두 번째 인자인 배열로 매개변수를 전달한다. 그리고 bind는 call, apply와 다르게 함수를 실행시키지 않고 새로운 함수를 반환하며 call과 같은 방식으로 매개변수를 전달한다. 또한 call, apply와 다르게 name 프로퍼티에 bound 접두어가 붙어 bind를 적용한 함수임을 쉽게 추적할 수 있다.

  • 화살표 함수란 무엇인가?
    익명함수의 모습을 하며, this를 자동으로 바인딩 하지 않기 때문에 this를 다루기 조금 더 수월하게 해준다. 가장 가까운 상위 스코프의 this를 이용한다.

  • 함수랑 메소드는 뭐가 다른가?
    함수는 개발자가 직접 관여하여 호출 주체 없이 실행되는 독립적인 기능이고 메소드는 객체에 속한 프로퍼티로서 호출되는 함수를 말한다

Profile picture

emewjin

Frontend Developer

잘못된 내용 혹은 더 좋은 방법이 있으면 언제든지 알려주세요 XD