[JS] 믿었던 this에 발등 찍히기 (1편)

Konseo·2025년 4월 24일
0

WEB

목록 보기
1/1
post-thumbnail

익숙한 개념일 수 있지만, 다시 한 번 곱씹어봅시다 😆 👄

javascript의 this

다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 가리킵니다. 클래스 내부에서만 사용할 수 있기 때문에, 혼란의 여지도 거의 없죠.

하지만 javascript에서는 얘기가 좀 다릅니다. JS의 this상황에 따라 바인딩되는 값이 달라지기 때문이에요.

함수의 호출 방식, 선언 위치, 실행 컨텍스트 등 여러 요소에 따라 this 가 참조하는 대상이 달라집니다. 즉, 어디서든 사용할 수 있지만, 또 항상 같은 걸 가리키는 건 아니라는 것 😅

그래서 this의 정확한 작동 방식을 이해하지 못하면 버그의 원인을 찾지 못하고 쓸데없이 시간만 낭비하기 쉽습니다.

👩🏻‍🏫 오늘은 javascript의 this가 맥락에 따라 어떻게 달라지고, 그로 인해 의도치 않게 코드가 불안정해지는 상황들을 짚어보려고 합니다 !

1 - 상황에 따라 달라지는 this

this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됩니다. 실행 컨텍스트는 함수를 호출할 때 생성되므로 바꿔 말하면 this는 함수를 호출할 때 결정됩니다.

결국 함수를 어떤 방식으로 호출하느냐에 따라 this의 값은 달라집니다.

대표적으로 this에 바인딩 되는 상황은 아래 3가지 입니다

  1. 전역공간에서의 this → 전역 객체를 가리킵니다
  2. 메서드 호출 시 메서드 내부의 this → 해당 메서드를 호출한 객체를 가리킵니다
  3. 함수 호출 시 함수 내부의 this지정되지 않습니다 (❗️)

여기서 우리는 3번에 주목할 필요가 있습니다. 1,2와 달리 3번은 this라는 값 자체가 지정되지 않고 있습니다.

왜 그런 걸까요? 🤨

this에는 호출한 주체에 대한 정보가 담긴다고 했습니다.

그런데 함수로서 호출하는 것은 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없습니다.

이렇게 this가 지정되지 않은경우 this는 전역 객체를 바라봅니다. 따라서 함수 호출 시 함수 내부의 this전역객체가 됩니다.

코드로 살펴봅시다!

const person = {
    name: 'john',
    say() {
        function printName() {
            console.log(this.name);
        }
        printName();
    }
};

person.say(); //undefined
  • person.say() 호출 시 내부 함수 printName() 이 호출 됨
  • printName() 앞에는 점(.)이 없음. 즉, 함수로서 호출되었으므로 this는 지정되지 않아 전역객체(window)를 가리킴
  • 전역객체에 name이란 속성은 존재하지 않으므로 undefined가 뜸

아마 위 코드를 짠 개발자는 undefined를 의도하고 짜지 않았을 가능성이 큽니다. 그렇다면 이 상황은 결국 의도치 않게 this 가 사용되버린 상황인 것이겠죠 !

음.. 우리는 이렇게 호출 주체가 없을 때 자동으로 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있었으면 좋겠습니다.

별다른 수 가 없을까요? 🤨 운좋게도, 의도대로 this 값을 받아올 수 있는 방법이 있습니다.

우회하여 this 바라보기

여러 방법이 있을 수 있겠지만, 그 중 가장 대표적인 방법은 바로 변수를 활용하는 것입니다.

const person = {
    name: 'john',
    say() {
        const that = this;  // this를 변수에 저장
        function printName() {
            console.log(that.name);  // that을 통해 this 우회
        }
        printName();
    }
};

person.say(); // john

위 예제의 say() 내부에서 thisthat이라는 변수에 저장하고, 이를 printName()에서 호출하면 개발자가 (아마도) 의도한대로 john이 출력됩니다.

사실 우회라고 할 수도 없을 만큼 허무한 방법이지만 기대에는 충실히 부합하고 있죠!

👩🏻‍🏫 사람마다 this, that, self, 등 제각각 다른 변수명을 쓰는데, 전통적으로 self 또는 that을 많이 씁니다. 원활한 협업이나 의사소통을 위해서는 널리 쓰이는 단어를 활용하는 것이 바람직합니다 ✨

‘this를 바인딩하지 않는’ 화살표 함수

만약 위와 같이 우회하여 this를 바라보는 방식이 영 탐탁지 않다면, 화살표 함수를 쓰는 것도 방법이 될 수 있겠습니다.

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수를 새로 도입했습니다.

화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어’ 상위 스코프의 this를 그대로 참조합니다.

const person = {
    name: 'john',
    say() {
        const that = this;  
        var printName = () => {
            console.log(that.name);  
        }
        printName();
    }
};

person.say(); // john

👩🏻‍🏫 JavaScript에서는 어떤 식별자(변수)를 찾을 때 현재 환경에서 그 변수가 없으면 바로 상위 환경을 검색합니다. 그렇게 점점 상위 환경으로 타고 타고 올라가다가 변수를 찾거나 가장 상위 환경에 도달하면 그만두게 되는 것이죠. 화살표 함수에서의 this 바인딩 방식도 이와 유사합니다.

2 - 신뢰할 수 없는 this (보안 위험 ⚠️)

우리는 위에서 살펴본 바와 같이, 상황에 따라 그때 그때 달라지는 this의 모습을 확인했습니다.

이 뿐만 아니라, this는 그 자체로 보안이나 신뢰성 위험을 초래할 수도 있습니다.

아래 코드를 살펴봅시다!

function pubsub(){
	const subscribers = []; // 구독자(함수)가 저장됨
	return {
		subscribe: function (subscriber){
			subscribers.push(subscriber);
		},
		publish : function (publication){
			const length = subscribers.length;
			for (let i=0; i<length; i+=1){
				subscribers[i](publication)
				}
			}
		};
	}

먼저 간략히 함수 내용을 해석해 봅시다.

pubsub 함수는 게시자-구독자(pub/sub) 패턴을 구현한 함수입니다.

이 패턴은 여러 구독자가 있고, 누군가가 어떤 소식을 게시하면, 모든 구독자들이 그 소식을 받는 구조입니다.

예를 들어, 위 패턴이 뉴스레터 시스템에 적용된다면

  • 사람들이 구독 버튼을 누르면 subscribe가 호출되고
  • 어떤 뉴스가 나오면 publish를 호출해서 모든 구독자에게 뉴스를 보내게 됩니다.

my_pubsub 객체를 만들어 구독자를 등록 하고 이후 publish()를 통해 뉴스 소식을 전달해 봅시다.

const my_pubsub = new pubsub();

// 구독자 등록 
my_pubsub.subscribe(function(data){
    console.log("A가 받은 소식:", data);
});

my_pubsub.subscribe(function(data){
    console.log("B가 받은 소식:", data);
});

// 소식 전달
my_pubsub.publish("JavaScript는 재밌다");

결과는 아래와 같이, 예상대로 출력됩니다.

A가 받은 소식: JavaScript는 재밌다
B가 받은 소식: JavaScript는 재밌다

다시 pubsub() 함수로 돌아가 봅시다. ⬇️


subscribers[i](publication) // 중요

위 문장은 메서드 호출같이 보이지 않지만, 사실 메서드 호출 입니다.

왜냐하면 JS에서 subscribers[i](publication)를 subscribers 배열의 프로퍼티(메서드)처럼 간주하기 때문입니다. 따라서 우리는 subscribers 배열에 대한 this 바인딩을 제공받습니다.

여기서 this 바인딩을 제공받을 수 있다는 것은, 달리말하면 모든 구독자가 this를 통해 subscribers 배열에 접근할 수 있다 🔥는 뜻이 되는데요.

여기서 만약 아래와 같이 pubsub() 함수에 info() 함수를 추가로 정의하여 구독자 수를 확인하는 기능을 추가하고,

const pubsub = (function(){
  const subscribers = [];
  return {
    ...(중략)...
    info : function(){
        console.log('구독자 수는 ' + subscribers.length + '명 입니다')
    }
  };
})

그리고 아까와 같이 my_pubsub 객체를 만들어 구독자를 등록하되 중간에 의도적으로 this를 통해 subscribers 배열에 접근하여 배열의 길이를 0으로 만든다면 어떻게 될까요 ? 🙄🙄

// 구독자 등록 
my_pubsub.subscribe(function(data){
    console.log("A가 받은 소식:", data);
});

my_pubsub.subscribe(function(data){
    console.log("B가 받은 소식:", data);
});

// 전체 구독자 수 확인
my_pubsub.info();

// 구독자 등록
my_pubsub.subscribe(function(data){
		console.log("C가 받은 소식:", data);
    this.length=0 // subscribers 배열에 의도적으로 접근 😈
    
});

// 소식 전달
my_pubsub.publish("JavaScript는 재밌다");

// 전체 구독자 수 재확인
my_pubsub.info();

결과는 아래와 같습니다. publish 이후에 구독자들이 모두 사라져 있습니다(...)

구독자 수는 2명 입니다.
A가 받은 소식: JavaScript는 재밌다
B가 받은 소식: JavaScript는 재밌다
C가 받은 소식: JavaScript는 재밌다
구독자 수는 0명 입니다.

이처럼 this 는 다른 함수를 위한 구독자들을 삭제하거나 메시지를 훔쳐보거나 하는 일을 얼마든지 저지를 수 있습니다.

우리는 위와 같은 상황을 피해야 합니다.
아무리 누군가가 의도된 악한 코드를 주입 시키더라도요!
여전히 문제가 발생할 수 있는 ‘여지’ 자체를 없애고 싶습니다.

여기서도 방법은 여러개이지만,

publish 함수는 for문을 forEach문으로 바꾸는 조금의 수정으로 문제를 해결할 수 있습니다

publish: function(publication){
	subscribers.forEach(function (subscriber){
		subscriber(pubalication)
	}
}

forEach 메서드는 콜백함수를 넘겨받게 되는데, 이때 콜백함수는 함수(호출)이기 때문에 this를 지정하지 않아, 전역객체를 참조합니다. 따라서 위 코드에서는 subscribers 배열에 접근하지 못합니다.

👩🏻‍🏫 물론 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하기도 합니다. 그래서 콜백 함수에서의 this는 ‘무조건 이거다!’라고 정의할 수 없습니다.

forEach 메서드를 사용해 이전 코드를 개선시켜봅시다.

function pubsub(){
	const subscribers = []; // 구독자(함수)가 저장됨
	return {
		subscribe: function (subscriber){
			subscribers.push(subscriber);
		},
		publish: function(publication){
			subscribers.forEach(function (subscriber){ // forEach문으로 변경
				subscriber(pubalication)
			}
		}
	}
}

const my_pubsub = new pubsub();

// 구독자 등록 
my_pubsub.subscribe(function(data){
    console.log("A가 받은 소식:", data);
});

my_pubsub.subscribe(function(data){
    console.log("B가 받은 소식:", data);
});

// 전체 구독자 수 확인
my_pubsub.info();

// 구독자 등록
my_pubsub.subscribe(function(data){
		console.log("C가 받은 소식:", data);
    this.length=0 // subscribers 배열에 의도적으로 접근 😈
    
});

// 소식 전달
my_pubsub.publish("JavaScript는 재밌다");

// 전체 구독자 수 재확인
my_pubsub.info();

결과는 아래와 같습니다. publish 이후에도 구독자가 잘 살아(?)있습니다 🔥

구독자 수는 2명 입니다.
A가 받은 소식: JavaScript는 재밌다
B가 받은 소식: JavaScript는 재밌다
C가 받은 소식: JavaScript는 재밌다
구독자 수는 3명 입니다.

👩🏻‍🏫 이렇게 보안에 위협을 받는 상황에서는 this 자체를 숨기거나 조작을 못하게 막는 것이 중요합니다! 여기서는 콜백함수로 방어했지만, call(), bind(), apply()등의 함수를 사용해서 명시적으로 this를 바꾸거나, 앞서 말한 화살표 함수를 사용하여 this가 lexical하게 상위 스코프를 따라가게끔 하는 것도 방법이 될 수 있겠습니다.

나만 잘 하(쓰)면 된다..?

모든 변수는 정적으로 바인딩되지만 오직 this만이 동적으로 바인딩되며, 결국 함수를 만든 쪽이 아닌 함수를 호출하는 쪽이 this 바인딩을 결정하게 됨을 배웠습니다.

결국 이러한 차이가 혼란을 가중시키고, 신뢰성 측면에서도 위협을 가하게 되는 것입니다.

카멜레온같이 시시각각 변하는 this 이지만, 다음의 규칙은 명시적 this 바인딩이 없는 한 늘 성립합니다.

1️⃣ 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조합니다.
2️⃣ 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조합니다.
3️⃣ 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다. 메서드의 내부함수에서도 같습니다.
4️⃣ 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조합니다

따라서 우리는 꾸준히 다양한 상황에서 this가 무엇일 지 예측해보는 연습을 한다면, 충분히 의도를 파악하여 오류나 보안 상의 위험을 피할 수 있기는 합니다.

this 없이도 Turing Complete 할 수 있을까?

하지만.. 위에서 살펴본 바와 같이 this는 그 자체로 개발자의 피로도를 높이게 하는 주요 원인이 될 수 있습니다.

실제도 2007년도에 js를 더 안전하게 사용하기 위한 연구가 몇 가지 진행되었는데, 여기서 다룬 가장 큰 문제 중 하나가 바로 this를 관리하는 방법이었다고 합니다.

자바스크립트의 구루(이자 json 창시자인) 클락포드는 this는 문제가 많고 필요도 없기에, this를 제거해도 js는 여전히 turing complete 하다고 얘기하고 있습니다.

이 글 에서도 this 없는 JS가 더 나은 함수형 프로그래밍 언어가 될 수 있다고 얘기하며, this가 제거된 다양한 구성 방식에 대해 소개 하고 있습니다.

👩🏻‍🏫 튜링완전(Turing complete)이란 한 언어가 튜링 기계를 시뮬레이션할 수 있는 경우 그 언어를 튜링 완전하다고 하는데, 일반적으로 한 언어가 다른 언어가 할 수 있는 일을 모두 할 수 있는지를 설명하는 용도로 사용됩니다. 예컨대, 자바스크립트가 튜링 완전하고, 자바가 튜링 완전하다면 자바스크립트는 자바로 만들 수 있는 모든 프로그램을 만들 수 있다는 뜻이 됩니다 !

마치며

this는 강력한 도구인 동시에, 예기치 않은 오류를 초래하는 원인이 되기도 합니다.

과연 우리는 this에 익숙해지며 계속 받아들여야 할까요, 아니면 점점 더 이를 지양하는 방향으로 나아가야 할까요?

그치만 적어도 한 가지 분명한 것은 this에 휘둘려서는 안 된다는 것입니다 🌀🙅🏻‍♀️

👩🏻‍🏫 다음편에는 의도적으로 this 를 제거하고 코드를 구성하는 방식에 대해 다뤄보겠습니다 !

reference

profile
둔한 붓이 총명함을 이긴다

0개의 댓글