그날 오후, 댄은 자신의 자리에서 잠시 일어나 사무실을 거닐었다. 수많은 모니터에서 뿜어져 나오는 열기가 공기를 데우고, 키보드 소리가 비처럼 쏟아지는 공간. 그는 동료 개발자인 레오의 자리 뒤에서 잠시 걸음을 멈췄다.
레오는 이제 막 주니어 딱지를 뗀, 열정 넘치는 개발자였다. 그의 모니터에는 댄이 조금 전 떠올렸던 Counter
컴포넌트가 떠 있었다. 레오는 코드를 더 깔끔하게 만들고 싶어 했다.
render
함수 안에 길게 쓰인 화살표 함수가 그의 눈에 거슬렸다.
onClick={() => this.setState({ count: this.state.count + 1 })}
“이 로직은 별도의 메서드로 빼내는 게 좋겠어.”
그는 좋은 개발자의 습관을 따라 코드를 리팩토링하기 시작했다. handleClick
이라는 이름의 메서드를 만들고, 상태를 변경하는 로직을 그 안으로 옮겼다.
class Counter extends React.Component {
// ... constructor ...
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.handleClick}>Click me</button>
</div>
);
}
}
훨씬 보기 좋았다. render
함수는 이제 화면을 그리는 책임에만 집중했고, 클릭 이벤트 처리는 handleClick
이라는 전문화된 메서드가 맡았다. 레오는 만족스러운 미소를 지으며 코드를 실행했다.
브라우저에 하얀 버튼이 나타났다.
그는 자신 있게 버튼을 클릭했다.
아무 일도 일어나지 않았다.
화면의 숫자는 여전히 ‘0’에 머물러 있었다.
레오의 미간에 희미한 주름이 잡혔다. 그는 개발자 도구의 콘솔 창을 열었다. 붉은색 에러 메시지가 그의 눈을 찔렀다.
TypeError: Cannot read property 'setState' of undefined.
setState
를 찾을 수 없다고?
에러 메시지는 handleClick
메서드 내부를 가리키고 있었다.
this.setState
에서 this
가 undefined
, 즉 존재하지 않는다는 의미였다.
“그럴 리가.”
레오는 혼란에 빠졌다. handleClick
은 분명 Counter
클래스 안에 정의된 메서드였다. 그렇다면 메서드 안의 this
는 당연히 Counter
컴포넌트 자신을 가리켜야 했다. 컴퓨터가 아주 기본적인 맥락을 놓치고 있는 것만 같았다. 그는 코드를 몇 번이고 다시 읽어봤지만, 논리적인 허점은 보이지 않았다.
바로 그때, 그의 곁을 지나가던 시니어 개발자가 모니터를 힐끗 보더니 무심하게 한마디 던졌다.
“아, this
문제네요. constructor
에서 bind
해줘야 해요.”
bind
?
레오는 반신반의하며 시니어가 알려준 대로 코드를 추가했다.
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this); // 이 한 줄을 추가
}
다시 코드를 실행하고 버튼을 클릭했다.
‘1’.
숫자가 마법처럼 올라갔다.
레오는 안도의 한숨을 내쉬었지만, 마음 한구석의 찜찜함은 가시지 않았다. 문제는 해결됐지만, ‘왜’ 해결됐는지 이해할 수 없었다. this.handleClick
에 자기 자신을 다시 할당하는 저 코드는 마치 고대의 마법사가 비밀스러운 주문을 외우는 것처럼 느껴졌다.
그 모습을 지켜보던 댄은 조용히 고개를 끄덕였다.
이것이 바로 클래스가 품고 있는 첫 번째 저주였다.
이것은 리액트의 잘못이 아니었다. 컴포넌트가 잘못된 것도 아니었다. 이것은 자바스크립트라는 언어 자체가 품고 있는, 오래고도 교활한 함정이었다. 자바스크립트에서 this
는 함수가 어디에 ‘정의’되었느냐가 아니라, 어떻게 ‘호출’되느냐에 따라 결정되었다.
버튼이 클릭되었을 때, handleClick
함수를 호출한 주체는 Counter
컴포넌트가 아니라 브라우저의 이벤트 시스템이었다. 그 순간, 함수는 자신의 주인을 잃어버렸고, this
는 허공으로 사라져 버렸다.
bind
는 바로 그 잃어버린 주인을 강제로 묶어주는 쇠사슬이었다. handleClick
함수의 this
가 영원히 Counter
컴포넌트만을 바라보도록 낙인을 찍는 행위.
댄은 생각했다.
‘개발자는 UI 로직에 집중해야 한다. 언어의 기묘한 동작 원리를 매번 기억하고, 보이지 않는 문맥을 바로잡기 위한 주문을 외워서는 안 된다.’
이 성가신 this
와의 전투는, 모든 리액트 개발자가 거쳐야 하는 통과의례이자, 낭비되는 지적 에너지였다. 그리고 이것은 앞으로 마주할 더 거대한 문제들의 시작에 불과했다.