본문 바로가기

웹 프론트엔드

[React] 함수형 컴포넌트는 왜 이벤트 부착 시점의 상태를 기억할까?

함수형 컴포넌트는 이벤트 부착시점의 상태를 기억한다.

함수형 컴포넌트와 클래스형 컴포넌트의 결정적인 차이를 중심으로 다룬다.

문제 상황

const [count, setCount] = useState(0);

useEffect(() => {
    window.addEventListener('unload', handleUnload);
  }, []);

const handleUnload = () => {
    localStorage.setItem("count", count);
};

return (
  <button onClick={() => setCount(count + 1)}>+</button>  
)

위와 같은 코드가 있다

button을 클릭했을 때 count가 1씩 증가하고,
화면을 닫을 때(unload), 로컬스토리지에서 count를 기억한다.
button을 5회 누른 후, 화면을 닫으면 로컬 스토리지에 "count"는 몇으로 저장될 것인가?

5라고 생각할 수 있지만,

답은 0이다.

처음엔 이것이 버그라고 생각해서, 대체 왜 안 되는지 이해할 수 없었다.

함수형 컴포넌트와 클래스형 컴포넌트의 차이

Dan Abramov가 말하길, 둘 간의 성능 차이는 거의 없다.

가독성 및 유지보수는 함수형 컴포넌트가 우수하다.

이 둘을 제외하고, 근본적인 차이는 무엇일까?
바로 "함수"와 "클래스"의 차이이다.

리액트까지 갈 필요 없이, JavaScript 입장에서 생각해주면 된다.

예시를 하나 들어보자.

페이스북 팔로우 하는 방황을 가정하자.

1. 현재 "시지프"의 프로필에 들어가있다.
2. "시지프" 팔로우 버튼을 누른다.
3. 팔로우 버튼은 수행되는데 3초가 걸린다.
4. 3초가 지나기 전에, "우테코" 프로필에 들어갔다.
5. 이때, 우리는 "시지프"가 팔로우 되길 원한다.
6. "우테코"가 팔로우 되어서는 안 된다.

클래스형 컴포넌트 예제코드를 살펴보자.

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

1. props로 user("시지프") 를 넘겨 받는다.
2. button을 누르면 follow할 수 있다.
3. follow에는 3초가 걸린다.
4. 3초가 지나기전에 props로 user("우테코")가 바뀌어서 내려왔다.

3초가 지난 후, alert로 "Followed 시지프" vs "Followed "우테코" 무엇이 뜰까?

정답은 "Followed 우테코"이다.
클래스형 컴포넌트의 경우, state 값이 this 아래에 있다. this 키워드로 어디서든 변경된 state에 접근이 가능하다.

함수형 컴포넌트 예제코드를 살펴보자.

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

3초가 지난 후, alert로 "Followed 시지프" vs "Followed "우테코" 무엇이 뜰까?
정답은 "Followed 시지프" 이다.

함수형 컴포넌트는 render 될 때의 값들을 유지한다.

React를 차치하고 JavaScript 만으로 바라보자.
클래스형 컴포넌트, class는 this가 있기 때문에, 변화된 값에 접근이 가능하다.
함수형 컴포넌트, props(인자)로 받는 값은 고정적이기 때문에, 넘겨받은 값을 그대로 기억한다.
클래스형 컴포넌트는 render()를 수행함으로써 리렌더링한다. 클래스 자체를 다시 선언되지 않는다.
함수형 컴포넌트는 함수 자체가 다시 선언된다.
이 관점에서 보면, 함수형 컴포넌트에서 이벤트 콜백함수가 실행되는 시점에 변화된 state 값을 기억하는 것은 불가능하다.
버그가 아니고, 함수의 identity이다.

함수형 컴포넌트에서 가장 최근의 state를 기억하는 방법은?

클래스형 컴포넌트에서 this와 비슷한 역할을 하는 것은 무엇일까?
바로 useRef이다. useRef는 lifecycle에 관계없이, 존재하는 값이다.
즉, 함수가 리렌더링 되든, mount되든, unmount되든 유지되는 값이다.

function MessageThread() {
  const [message, setMessage] = useState('');

  // 최신값을 쫓아간다
  const latestMessage = useRef('');

  useEffect(() => {
    latestMessage.current = message;
  });

  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };

이와 같이 쓴다면, 최신 값을 불러올 수 있다.

문제 상황 해결

맨 처음 문제 상황을 useRef를 활용하여 해결해보자.

const [count, setCount] = useState(0);
const countRef = useRef(null);

useEffect(() => {
  countRef.current = count;
}, [count])

useEffect(() => {
    window.addEventListener('unload', handleUnload);
  }, []);

const handleUnload = () => {
    localStorage.setItem("count", countRef.current);
};

return (
  <button onClick={() => setCount(count+1)}>+</button>  
)

이제 button을 5회 누른 후,
화면을 닫으면 localStorage에 "count": 5 를 확인할 수 있을 것이다.

참고

함수형 컴포넌트와 클래스, 어떤 차이가 존재할까?