리액트의 함수형 컴포넌트와 클래스는 어떻게 다를까?
이 두 컴폰넌트 간의 근본적인 차이에 대해서 얘기해보려고 한다.
함수형 컴포넌트는 렌더링된 값들을 고정시킨다
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
위 컴포넌트에 있는 버튼은 setTimeout
을 이용해 네트워크 요청을 보내고 확인 창을 띄워주는 역할을 한다. 이 컴포넌트를 클래스형으로 만들어보자.
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
대부분의 사람들은 이 코드가 동일하다고 생각할 것이다. 하지만 이 둘은 미묘한 차이를 가지고 있다.
예제 테스트 다음 예제를 열어서 확인해보자.
다음과 같은 순서로 실행해보자.
결과를 확인해보면 차이점을 발견했을 것이다.
Dan의 프로필에서 함수형 컴포넌트의 Follow 버튼을 누른 후 Sophie의 프로필로 이동하면 알림창에서는 'Followed Dan'이라고 쓰여져 있다.
반면, 클래스형 컴포넌트에서는 똑같이 했을 때, 'Followed Sophie'라고 쓰여진걸 볼 수 있다.
이 예제에서는 함수형 컴포넌트의 경우가 올바른 케이스이다.
왜냐면 내가 어떤 사람을 팔로우하고 다른사람의 프로필로 이동했다 하더라도 컴포넌트가 이를 헷갈려해서는 안된다. 클래스 컴포넌트의 동작은 Bug다!
왜 이렇게 동작할까?
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
이 메서드는 this.props.user
로부터 값을 불러온다. Props는 리액트에서 불변한 값이지만, this는 변경 가능하며 조작할 수 있다.
따라서 요청이 진행되고 있는 상황에서 클래스 컴포넌트가 다시 렌더링 된다면 this.props
또한 바뀐다. showMessage
메서드가 새로운 props
의 user
를 읽는 것이다.
다시말해서, UI가 현재 애플리케이션 상태를 보여주는 함수라 한다면, 이벤트 핸들러 또한 시각적 컴포넌트와 같이 렌더링 결과의 한 부분인 것이다.
즉 이벤트 핸들러가 어떤 props와 state를 가진 render에 종속된다는 것이다.
하지만 this.props
를 읽는 콜백을 가진 timeout이 사용되면서 그 종속관계가 깨져버렸다. showMessage
콜백은 더이상 어떤 render에도 종속되지 않게 됐고, 올바른 props 또한 잃게 되었다.
함수형 컴포넌트라는 개념이 없다고 가정했을때 이 문제를 어떻게 해결할 수 있을까?
이를 위해서는 render
와 올바른 props
그리고 이들을 읽는 showMessage 사이의 관계를 다시 바로 잡아 주어야 한다.
한 가지 방법은 this.props
를 조금 더 일찍 부르고 timeout 함수에게는 미리 저장해놓은 값을 전달하는 것이다.
이렇게 하는 것은 클래스의 장점을 무색하게 만들고 복잡하게 만든다.
이 방법 이외에도 render
에서 props
와 state
를 클로저로 감싸준다면 우리가 원하는 방식으로 동작하게 할 수 있다.
class ProfilePage extends React.Component {
render() {
// props의 값을 고정!
const props = this.props;
// Note: 여긴 *render 안에* 존재하는 곳이다!
// 클래스의 메서드가 아닌 render의 메서드
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
이 방법은 매우 잘 동작하지만 조금 꺼림칙하다. 메서드를 클래스에서 선언하지 않고 render
내부에서 선언할건데 굳이 클래스를 이용할 필요가 있냐라는 생각이든다.
이를 함수형 컴포넌트로 다시 선언해보자.
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
값이 인자로 전달됐기 대문에 아까와 마찬가지로 props
는 보존된다. 클래스의 this
와 다르게 함수가 받는 인자는 리액트가 변경할 수 없다.
Hooks의 state에서도 같은 원리가 적용된다.
function MessageThread() {
const [message, setMessage] = useState('');
const showMessage = () => {
alert('You said: ' + message);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e) => {
setMessage(e.target.value);
};
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
메세지 전송이 이루어졌을 때 컴포넌트는 어떤 메세지가 전송되었는지를 헷갈려서는 안된다. 이 함수의 메세지는 클릭핸들러가 호출됐을 때의 state를 고정시켜둔다. 때문에 내가 Send를 눌렀을 당시의 input 메세지 값을 간직할 수 있게된다.
지금까지 리액트에서 함수가 props와 state 값을 유지한다는 것에 대해 알아보았다. 그런데 만약 특정 redner에 종속된 것 말고 가장 최근의 state나 props를 읽고 싶다면 어떻게 해아할까?
클래스에서는 this
가 변할 수 있는 값이기 때문에 this.props, this.state를 읽어오면 된다. 그런데 함수형 컴포넌트에서도 this
처럼 변할 수 있고 서로 다른 render
들끼리 공유할 수 있는 녀석이 하나 있다.
이 친구는 ref
라고 부른다.
function MyComponent() {
const ref = useRef(null);
// `ref.current`로 읽고 쓸 수 있다.
// ...
}
하지만 ref는 this와 다르게 직접 관리해줘야한다.
ref는 클래스의 인스턴스 영역과 같은 역할을 수행한다. 이는 무언가를 넣을 수 있는 박스라고 봐도 좋다.
리액트의 ref가 자동으로 state나 props를 최신값으로 유지하는 것은 아니다. 일반적으로 이러한 기능을 쓰게 되는 경우는 드물기 때문에 이를 기본동작으로 두는 것은 비효율 적이다.
function MessageThread() {
const [message, setMessage] = useState('');
const latestMessage = useRef('');
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e) => {
setMessage(e.target.value);
latestMessage.current = e.target.value;
};
우리가 showMessage
로 부터 message를 읽는다면 우리가 send버튼을 눌렀을 때의 message를 볼 수 있다. 하지만 latestMessage.current
를 읽는다면 우리는 가장 최근에 보내진 메세지 값을 읽어 올 수 있다.
ref는 고정된 값이 아니기 때문에 렌더링 도중에 읽거나 쓰는 것은 피하는 것이 좋다.
function MessageThread() {
const [message, setMessage] = useState('');
// 최신값을 쫓아간다
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};