클래스형 컴포넌트에서 이벤트를 어떻게 다루는지에 대한 수업을 받았다. 그중에서도 이번 포스트에서는 새로 알게된 사항에 대해 정리하고자 한다.
기본 DOM 요소에만 이벤트 설정
React 에서의는 자체적으로 만든 컴포넌트가 아닌 기본 DOM 요소에만 이벤트를 설정할 수 있다. 그저 <MyButton onClick={activeEvent}>
라고만 하면 MyButton 에 onClick 을 통해 클릭 이벤트를 설정하는 것이 아닌 onClick 이라는 이름의 props 를 전달하게 된다. 그래서 자체적으로 만든 컴포넌트에 이벤트를 설정하려면 이벤트를 props 로 전달하고 그 컴포넌트 내부에서 기본 DOM 요소에 전달해야 한다.
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
export default function Event() {
const handleClick = () => {
console.log("test");
};
const handleClick2 = (e, str) => {
console.log("e : ", e);
console.log(str);
};
const handleClick3 = () => {
console.log("test3");
};
return (
<>
<button onClick={handleClick}>클릭</button>
<button onClick={(e) => handleClick2(e, "클릭함")}>클릭2</button>
<Button onClick={handleClick3}>클릭3</Button>
</>
);
}
클래스 컴포넌트의 state 는 반드시 객체여야 한다. 이때 state 를 변경하기 위해서 setState 를 사용해야 한다.
state 를 사용하고자 할때 constructor 를 사용할때와 사용하지 않을때의 코드가 다르다.
constructor
를 사용할때
class Counter extends Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// ...
}
constructor 를 사용시 super(props) 를 통해 부모 컴포넌트로부터 전달받은 props 를 super 를 통해 상속받은 Component 의 constructor 로 전달해줘야 하며 메서드의 경우 bind 처리가 필요하다.
constructor
를 사용하지 않을때
class Counter extends Component {
state = {
age: 42,
};
handleAgeChange = () => {
this.setState({
age: this.state.age + 1
});
};
render() {
return (
<>
<button onClick={this.handleAgeChange}>
Increment age
</button>
<p>You are {this.state.age}.</p>
</>
);
}
}
주의점이 있다. constructor
를 사용하지 않고 이벤트 핸들러 메서드를 만들었을 경우 화살표함수의 형태로 사용해줘야 한다.
class Counter extends Component {
state = {
age: 42,
};
handleAgeChange() {
this.setState({ // Error !! this = undefined
age: this.state.age + 1
});
};
render() {
return (
<>
<button onClick={this.handleAgeChange}>
Increment age
</button>
<p>You are {this.state.age}.</p>
</>
);
}
}
이렇게 일반 함수의 형태를 사용할 경우 해당 함수에 대한 binding 처리가 되어있지 않아서 이벤트 핸들러 메서드 내에서 this 를 가져오려 할때 에러가 발생한다.
그래서 앞에서 소개한 것처럼 constructor 에 bind 처리를 해주는 방식으로 해결할 수 있으나 이벤트 핸들러를 추가할 때마다 생성자함수에 매번 추가해줘야 하는 수고가 발생한다.
class Counter extends Component {
state = {
age: 42,
};
handleAgeChange() {
this.setState({
age: this.state.age + 1
});
};
render() {
return (
<>
<button onClick={this.handleAgeChange.bind(this)}>
Increment age
</button>
<p>You are {this.state.age}.</p>
</>
);
}
}
혹은 이처럼 onClick 할때 명시적으로 binding 할수도 있는데 가독성이 떨어지긴 해도 성능 이슈를 야기하지는 않는다.
class Counter extends Component {
state = {
age: 42,
};
handleAgeChange() {
this.setState({
age: this.state.age + 1
});
};
render() {
return (
<>
<button onClick={() => this.handleAgegChange()}>
Increment age
</button>
<p>You are {this.state.age}.</p>
</>
);
}
}
아니면 이처럼 콜백에 화살표 함수를 사용해도 되는데 이 경우 새로 렌더링 될 때마다 콜백 객체가 새로 생성된다. 그래서 props 로 해당 이벤트 핸들러를 전달시 매번 새로운 객체가 생성되어서 불필요한 재렌더링을 유발할 수 있다.
class Counter extends Component {
state = {
age: 42,
};
handleAgeChange = () => {
this.setState({
age: this.state.age + 1
});
};
render() {
return (
<>
<button onClick={this.handleAgegChange}>
Increment age
</button>
<p>You are {this.state.age}.</p>
</>
);
}
}
그래서 처음부터 이벤트 핸들러를 화살표 함수로 만들기도 한다. 이 경우 가독성이 좋고 생성자에 매번 binding 하는 코드를 추가할 필요도 없고 콜백 객체를 매번 생성하지도 않아서 불필요한 렌더링을 유발하지 않는다.
그러나 화살표함수가 만능은 아니다. 왜냐하면 화살표 함수와 일반 함수의 트랜스파일 결과물이 다르기 때문이다.
import { Component } from "react";
class EventClass extends Component {
constructor(props) {
super(props); // 부모 클래스인 Component의 생성자를 호출한다.
this.handleClickNormal = this.handleClickNormal.bind(this);
}
handleClickNormal() {
console.log("클래스 컴포넌트", this);
}
handleClickArrow = () => {
console.log("화살표 함수", this);
};
render() {
return (
<>
<button onClick={this.handleClickNormal}>클릭클래스</button>
<button onClick={this.handleClickArrow}>화살표 함수</button>
</>
);
}
}
export default EventClass;
일반 함수와 화살표 함수를 실행시의 this 를 출력해서 Prototype 을 비교해봤다.
일반 함수 | 화살표 함수 |
---|---|
화살표함수는 일반함수와 달리 프로토타입 메서드로 등록되지 않았다. 화살표 함수를 사용시 this 라는 변수가 없기 때문에 그 상위 환경에서의 this 를 가리키게 된다.
프로토타입이 헷갈린다면 이전에 프로토타입에 대해 정리했던 포스트1, 포스트2 를 참고하자.
EventClass 가 Component 를 extend 로 상속받고 있으니 EventClass 의 Prototype 은 Component 이다.
그런데 이때 일반함수와 화살표함수가 헷갈리기 시작한다. 화살표 함수는 상위환경에서의 this 를 가리킨다고 하는데 정작 위에서 표로 비교한 것을 보면 프로토타입에 등록되는지 여부의 차이만 존재하지 나머지는 모두 동일하다... 뭘까?
자바스크립트의 함수는 호출될 때 매개변수로 전달되는 인자값 외에 arguments 객체와 this 를 암묵적으로 전달받는다.
function square(number) {
console.log(arguments);
console.log(this);
return number * number;
}
square(2);
Java 에서의 this 키워드는 인스턴스 자신을 가리키는 참조변수이다. 그래서 아래의 Java 코드에서 생성자 함수 내의 this.name 은 멤버변수를 말하고 name 은 생성자 함수가 전달받은 매개변수를 의미하여 매개변수와 멤버변수명이 겹칠때 this 를 사용한다.
public Class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
자바스크립트의 this 는 함수 호출방식에 따라 this 에 바인딩되는 객체가 달라진다. 이게 헷갈리는 포인트다...
함수 호출 방식
1. 함수 호출
2. 메소드 호출
3. 생성자 함수 호출
4. apply/call/bind 호출
var foo = function() {
console.dir(this);
};
// 1. 함수 호출
foo(); // window
// 2. 메소드 호출
var obj ={ foo: foo };
obj.foo(); // obj
// 3. 생성자 함수 호출
var instance = new foo(); // instance
// 4. apply/call/bind 호출
var bar = { name: 'bar' };
foo.call(bar); // bar
foo.apply(bar); // bar
foo.bind(bar)(); // bar
case | 사진 |
---|---|
1. 함수 호출 | |
2. 메소드 호출 | |
3. 생성자 함수 호출 | |
4. apply/call/bind 호출 |
console.dir()
: console.dir 메소드는 주어진 JavaScript 객세 속성을 인터랙티브한 목록으로 표시하며 이를 통해 객체의 속성을 쉽게 확인할 수 있다.
자바스크립트에서 this 는 기본적으로 전역객체에 ( browser-side : window
, server-side(Node.js) : global
) 바인딩된다.
this
는 전역객체를 바인딩한다.메소드 내부의 this 는 해당 메소드를 소유한 객체이자 해당 메소드를 호출한 객체에 바인딩된다.
var obj1 = {
name: 'Lee',
sayName: function() {
console.log(this.name);
}
}
var obj2 = {
name: 'Kim'
}
obj2.sayName = obj1.sayName;
obj1.sayName();
obj2.sayName();
자바스크립트에서 생성자 함수는 객체를 생성하는 역할을 하는데 기존 함수에 new 키워드만 붙여서 호출하면 해당 함수가 생성자 함수로 동작한다. 그러나 혼란을 방지하기 위해 생성자함수의 첫문자는 대문자로 기술한다.
function Person(name) {
this.name = name;
}
var me = new Person('Lee');
console.log(me);
var you = Person('Kim'); // new 키워드와 함께 호출해야 생성자 함수로 동작
console.log(you); // undefined
new 키워드로 생성자 함수가 실행되기 전 빈 객체가 생성된다. 이 빈 객체가 생성자 함수가 생성하는 객체인데 이후 생성자 함수 내에서 사용되는 this 는 이 빈 객체를 가리킨다. 이 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.
생성된 빈 객체에 this 를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다. this 는 새로 생성한 빈 객체를 가리키고 있으니 this 를 통해 동적으로 생성한 프로퍼티, 메소드는 새로 생성된 객체에 추가된다.
반환문이 없을때 암묵적으로 this 를 반환하는데 명시적으로 this 를 반환해도 같은 결과가 나온다. 그런데 명시적으로 this 가 아닌 객체를 반환할 때 해당 객체가 반환되나 생성자 함수로서 역할을 수행하지 못한다. 그래서 생성자함수는 명시적으로 반환문을 사용하지 않는다.
Function.prototype.apply
, Function.prototype.call
메소드는 this 를 특정 객체에 명시적으로 바인딩할 수 있게 해준다. 이들은 모든 함수 객체의 프로토타입 객체인 Function.prototype 객체의 메소드이다.
func.apply(thisArg, [argsArray])
// thisArg : 함수 내부의 this 에 바인딩할 객체
// argsArray : 함수에 전달할 argument 배열
apply 함수를 호출하는 주체는 func 함수이며 apply() 메서드는 thisArg 를 함수 내부의 this 에 바인딩할 뿐 본질적인 기능은 함수 호출이다.
var Person = function(name) {
this.name = name;
}
var foo = {};
Person.apply(foo, ['name']);
console.log(foo);
call()
메소드는 apply() 와 기능이 같으나 두번째 인자를 배열이 아니라 각각 하나의 인자로 넘긴다.
Person.apply(foo, [1, 2, 3]);
Person.call(foo, 1, 2, 3);
Function.prototype.bind
는 함수에 인자로 전달한 this 가 바인딩된 새로운 함수를 리턴한다. 즉 Function.prototype.bind
는 Function.prototype.apply
, Function.prototype.call
메소드처럼 함수를 실행하지 않아서 명시적으로 함수를 호출할 필요가 있다.
생성자 함수와 객체의 메소드, apply, call, bind 를 제외한 모든 함수 내부의 this 는 전역 객체를 가리킨다.
이런 상황에서 맨 처음에 클래스 컴포넌트에서 일반 함수의 경우 이벤트 핸들러를 생성자 함수 내에서 this 바인딩했던 코드를 다시 살펴보자.
import { Component } from "react";
class EventClass extends Component {
constructor(props) {
super(props); // 부모 클래스인 Component의 생성자를 호출한다.
this.handleClickNormal = this.handleClickNormal.bind(this);
}
handleClickNormal() {
console.log("클래스 컴포넌트", this);
}
handleClickArrow = () => {
console.log("화살표 함수", this);
};
render() {
return (
<>
<button onClick={this.handleClickNormal}>클릭클래스</button>
<button onClick={this.handleClickArrow}>화살표 함수</button>
</>
);
}
}
export default EventClass;
위의 클래스의 경우 함수는 아니지만 constructor 생성자 함수 내에 Function.prototype.bind 가 사용되었다.
생성자 함수 내에서의 this 는 새로 생성하는 빈 객체를 가리키고 this 를 통해 동적으로 프로퍼티를 생성후 생성된 객체를 반환한다.
즉, EventClass 를 new 키워드로 다른 곳에서 생성한다면 그때 생성자함수가 실행되며 this 는 새로 생성한 빈 객체를 가리키고 bind 메소드는 함수에 인자로 전달한 this 가 바인딩된 새로운 함수를 리턴하므로 EventClass 를 new 키워드로 새로 생성한 빈 객체에 (this 가 가리킴) 동적으로 메소드 (이벤트 핸들러 handleClickNormal) 를 생성하여 (this 가 바인딩된 handleClickNormal) 새로 생성된 객체를 반환한다.
자바스크립트는 호출방식에 의해 this 에 바인딩할 객체가 동적으로 결정된다. 즉, 함수 선언시 바인딩할 객체가 정적으로 결정되는 것이 아니고 함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this 에 바인딩할 객체가 동적으로 결정된다.
화살표함수는 함수를 선언할 때 this 에 바인딩할 객체가 정적으로 결정된다. 이게 일반함수와 화살표함수의 주요 차이점인데 일반 함수는 호출되는 컨텍스트에 따라 this 가 동적으로 결정된다면 화살표 함수는 자신이 선언된 시점에서의 this 를 캡쳐하고 사용한다. 달리 표현하자면 화살표 함수의 this 는 언제나 상위 스코프의 this 를 가리키는데
이를 Lexical this
라고 한다. 이런 방식은 렉시컬 스코프와 유사하다.
함수의 상위 스코프를 결정하는 렉시컬 스코프는 함수를 선언시 결정된다. this 는 함수가 호출되는 방식에 따라 바인딩할 객체가 동적으로 결정된다.
또한 화살표함수는 프로토타입에 등록되지 않는데 화살표함수는 클래스의 인스턴스 메서드로만 동작하고 프로토타입 메서드로는 동작하지 않는다.
화살표함수는 call, apply, bind 메소드를 사용하여 this 를 변경할 수 없다.
화살표함수가 Lexical this 를 갖기 때문에 person 객체를 가리키지 않고 전역객체 window 를 가리키게 된다. 대신 ES6 의 축약 메소드 표현을 사용하는 편을 권장한다.
// Bad
const person = {
name: 'Lee',
sayHi: () => console.log(`Hi ${this.name}`)
};
person.sayHi(); // Hi undefined
// Good
const person = {
name: 'Lee',
sayHi() { // === sayHi: function() {
console.log(`Hi ${this.name}`);
}
};
person.sayHi(); // Hi Lee
// Bad
const person = {
name: 'Lee',
};
Object.prototype.sayHi = () => console.log(`Hi ${this.name}`);
person.sayHi(); // Hi undefined
// Good
const person = {
name: 'Lee',
};
Object.prototype.sayHi = function() {
console.log(`Hi ${this.name}`);
};
person.sayHi(); // Hi Lee
화살표함수는 생성자 함수로 사용할 수 없다. 화살표함수는 prototpye 프로퍼티를 갖고 있지 않다. 이에 반해 생성자함수는 prototype 프로퍼티를 가지며 prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor 를 사용한다. 그렇게 해서 클래스형 컴포넌트 예시코드에서 constructor 의 최상단에 프로토타입 객체인 Component 의 생성자함수 constructor 를 쓰고자 super(props)
의 형태를 쓴 것으로 보인다.
addEventListener 함수의 콜백함수를 화살표 함수로 사용시 this 가 상위 컨텍스트인 전역 객체 window 를 가리키게 되니 function 키워드로 정의한 일반 함수를 사용해야 한다. 이때의 this 는 이벤트 리스너에 바인딩된 요소를 가리킨다.
// Bad
var button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});
// Good
var button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});
마지막으로 다시 헷갈렸던 부분을 살펴보자.
import { Component } from "react";
class EventClass extends Component {
constructor(props) {
super(props); // 부모 클래스인 Component의 생성자를 호출한다.
this.handleClickNormal = this.handleClickNormal.bind(this);
}
handleClickNormal() {
console.log("클래스 컴포넌트", this);
}
handleClickArrow = () => {
console.log("화살표 함수", this);
};
render() {
return (
<>
<button onClick={this.handleClickNormal}>클릭클래스</button>
<button onClick={this.handleClickArrow}>화살표 함수</button>
</>
);
}
}
export default EventClass;
handleClickNormal
는 일반함수이나 bind 를 통해 this 를 바인딩해주고 있고 클래스의 메소드는 해당 클래스의 프로토타입 객체에 저장되고 모든 인스턴스가 이 프로토타입 객체를 공유하기에 메모리 효율성과 성능 개선 측면에 유리한 부분이 있다. 이러한 부분은 super(props) 를 호출하는 과정에서 발생하게 된다.
그래서 handleClickNormal
는 인스턴스 메소드로 프로토타입 객체에 바인딩되어 저장된 것이다.
반면 handleClickArrow
는 클래스 필드로 정의되어 있어 클래스 필드로 정의된 메소드는 인스턴스에 바인딩되며 프로토타입 객체에 저장되지 않는다. 그래서 handleClickNormal 만 프로토타입 객체인 Component 에서 볼 수 있는 것이다...
docs
react.dev 참고
reference
poiemaweb - arrow function
poiemaweb - this
blog
React 클래스 컴포넌트에서 bind 메서드 사용하기