함수형 컴포넌트와 클래스 컴포넌트는 어떤 점이 다를까요? 문법적인 차이점은 쉽게 찾을 수 있겠지만 오늘은 내부적으로 어떤 점이 다른지 알아보겠습니다.
아래 컴포넌트를 살펴봅시다.
위 컴포넌트에 있는 버튼은 setTimeout
을 이용해 네트워크 요청을 보내고 확인 창을 띄워주는 역할을 합니다. 예를 들어 props.user
가 Dan
이라면, 버튼을 누르고 3초 뒤에 Follwed Dan
이라는 창이 띄워집니다.
이 컴포넌트를 클래스로 만들면 다음과 같습니다.
대부분의 사람들은 위 두 컴포넌트가 동일하다고 생각할 것입니다. 하지만 두 코드는 미묘하게 다릅니다.
이 둘의 차이점은 Live demo에서 확인할 수 있습니다.
이 둘의 차이점이 나타나는 상황은 다음과 같습니다.
- Follow 버튼을 누르고,
- 3초가 지나기 전에 선택된 프로필을 바꿉니다.
- 알림창의 글을 읽어봅시다.
함수형 컴포넌트에서는 바꾸기 전 프로필을 팔로우 했다고 뜨는 반면, 클래스 컴포넌트는 3초가 지나기 전에 선택한 프로필을 팔로우 했다고 나옵니다.
이 경우, 클래스가 보여준 동작은 명백한 버그입니다. 왜 이렇게 동작하는 걸까요?
클래스의 showMessage
메서드를 살펴보겠습니다.
이 메서드는 this.props.user
로부터 값을 불러옵니다. Props는 리액트에서 불변한 값입니다. 하지만, this
는 변경 가능하며, 조작할 수 있습니다.
따라서 요청이 진행되고 있는 상황에서 클래스 컴포넌트가 다시 렌더링 된다면 this.props
또한 바뀝니다.
엄밀히 말하면, this.props
가 바뀌는 것이 아니라 this
가 바뀝니다.
클래스는 객체를 생성하는 생성자 함수입니다. 클래스를 new 연산자와 함께 호출하면 인스턴스 객체가 생성됩니다. 그리고, 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리키게 됩니다.
리액트는 내부적으로 클래스 컴포넌트가 렌더링될 때 클래스를 new 연산자와 함께 호출하여 인스턴스 객체를 생성하고, 렌더 함수를 호출합니다. 이를 코드로 표현하면 다음과 같습니다.
class ProfilePage extends React.Component {
...
}
const Dan = new ProfilePage();
Dan.render();
그러므로 클래스 컴포넌트 내부에서 this는 인스턴스 객체를 가리키고, render() 함수 내부에서도 this는 인스턴스 객체를 가리키게 됩니다.(일반 함수로서 호출한 경우 this는 메서드를 호출한 객체를 가리키기 때문입니다.) 클래스 컴포넌트는 매번 이렇게 인스턴스 객체를 생성하며 호출되고 그럴때마다 this는 다른 객체를 가리키게 됩니다. 이 때문에 Follwed Dan
이 아니라 Followed Sophie
가 쓰여지는 오류가 발생합니다.
이 상황에서 함수형 컴포넌트가 없다고 가정했을 때 어떤 해결 방법이 있을까요?
한 가지 방법은, this
의 정보를 미리 변수에 담아두는 방법입니다.
처음 클래스 컴포넌트를 호출했을 때 this.props.user
값을 변수에 담아두고 이 값을 인자로 전달합니다. 그러면 잘 동작하는 것을 알 수 있습니다. 하지만 이런 방법은 코드의 복잡도를 높이고 에러에 노출될 가능성이 높아집니다.
또 다른 방법은, 클로저로 이를 해결할 수 있습니다.
(여기서 말하는 클로저는 스코프에 따라서 내부함수의 범위에서는 외부 함수 범위에 있는 변수에 접근이 가능하지만 그 반대는 실현이 불가능하다라는 개념으로만 받아들이면 될 것 같습니다.) 여기서도 this.props
값을 변수에 담아두고 render() 함수 내부로 메서드를 옮겼습니다. 이렇게 하면 render될 당시의 props 값을 그대로 사용할 수 있습니다.
그런데, 이 방법은 render() {}
부분을 지우고 클래스를 함수로 바꾸면 그대로 함수형 컴포넌트가 됩니다. 이럴 거면 굳이 클래스를 사용할 필요가 있나라는 생각이 듭니다.
이로써, 리액트 클래스와 함수형 컴포넌트 사이의 큰 차이점을 이해하게 됐습니다.
함수형 컴포넌트는 render 될 때의 값들을 유지합니다.
만약, 함수형 컴포넌트에서 나중에 render될 값을 미리 가져와서 쓰고 싶다면 useRef
를 사용할 수 있습니다.(클래스라면 this.state를 읽어오면 됩니다.)
function MessageThread() {
const latestMessage = useRef('');
const showMessage = () => {
alert(latestMessage.current);
};
}
latestMessage.current
를 읽는다면 가장 최근에 보내진 메시지 값을 읽어올 수 있습니다.
이 글은 댄 아브라모프의 블로그 글을 보고 정리한 글입니다.
https://overreacted.io/ko/how-are-function-components-different-from-classes/