왜 리액트를 사용하면서 클래스 컴포넌트에서 어떤 점의 불편함 때문에 함수 컴포넌트를 사용하는지에 대한 고민을 해보지 못한 것 같아 작성하게 되었다.
처음에 가졌던 생각은 함수 컴포넌트에 비해 지켜야 할 규칙이 적어 코드의 길이가 줄어든다는 점 때문에 결과적으로 자바스크립트 파일이 작아져 네트워크상에서 이점을 가져갈 수 있다고 생각했다.
그런데 이런 고민에 대한 해답을 문서에서 찾을 수 있었다.
It’s hard to reuse stateful logic between components
컴포넌트 사이에서 상태 로직을 재사용하기 어렵다고 한다. 물론 고차 컴포넌트 방식을 사용하여 해결할수도 있지만 이러한 방식도 결국 컴포넌트의 재구성을 요구해야해서 번거롭기도하면서 코드의 추적을 어렵게 한다고 한다.
하나의 예시로 마우스를 클릭하면 카운트가 증가되는 로직을 가진 컴포넌트와 마우스를 호버하면 카운트가 증가되는 로직을 가진 또 다른 컴포넌트가 있다고 가정해보자.
class Counter1 extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
const { count } = this.state;
return <button onClick={this.increment}>{count}번 클릭!</button>;
}
}
class Counter2 extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
const { count } = this.state;
return <div onMouseOver={this.increment}>{count}번 호버!</div>;
}
}
코드를 참고해보면 굉장히 중복되는 코드가 많은 것을 볼 수 있다.
클래스 컴포넌트에서 이를 해결하기 위해 주로 HOC 방식을 사용하게 될텐데 그러면 다음과 같이 작성할 수 있다.
function withCounter(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
return <WrappedComponent count={this.state.count} increment={this.increment} {...this.props} />;
}
};
}
그리고 사용하고자 하는 컴포넌트에서는 아래와 같이 작성해주면 된다.
class Counter1 extends Component {
render() {
const { count, increment } = this.props;
return <button onClick={increment}>{count}번 클릭!</button>;
}
}
export default withCounter(Counter1);
하지만 HOC을 사용했을 때, wrapper hell이라는 또 다른 문제가 발생한다.
export default withOne(withTwo(withTree(Component)))
그래서 이런 wrapper hell을 피하고 상태 관련 로직을 재사용할 수 있게 하고자 hook이 나오게 된 것이고 함수형을 쓰는 주된 이유 중 하나가 된 것이다.
import { useEffect, useState } from ’react’;
export function useScreenWidth(): number {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = (e: any) => {
setWidth(e.target.innerWidth);
};
window.addEventListener(’resize’, handler);
return () => {
window.removeEventListener(’resize’, handler);
};
}, []);
return width;
}
위의 코드와 같이 커스텀 훅으로 따로 빼두면 이러한 상태 관련 로직을 컴포넌트 사이에서 쉽게 재사용할 수 있게 된다. 👍
그리고 추가적으로 this가 mutable하기 때문에 클래스 컴포넌트에서 발생할 수 있는 문제를 보여주는 좋은 예시가 있어서 읽어보면 좋을 것 같다.