React에서 Functional Component를 사용하면 useState hook을 정~말 많이 사용하게 된다.
그 중, 프로젝트를 하면서 setState를 사용하면서 들었던 두 가지 의문점과, 이를 해결해나가는 과정 그리고 그에 관한 생각들을 기술해보려고 한다.
🌱 첫 번째 의문 - setState는 어떻게 동작할까?
사실 지금까지 이미 만들어져있는 hook들을 사용하면서, 이 hook들의 세부 동작에 대해서는 크게 고민하지 않고 사용했다.
몇 번 궁금해서 찾아보기는 했으나, 항상 적당한 수준의 이해만 하고 넘어가기를 반복해서 잘 이해했다고 보기는 어려웠다.
setState는 비동기로 동작한다. 왜냐하면 React에서 state가 변경될 때마다 매번 바로바로 DOM을 리렌더링하게되면 퍼포먼스 효율성이 떨어지기 때문에, React에서 판단하기에 가장 적절한 시기에 DOM을 리렌더한다. 정도만 알고있었다.
항상 걸렸던 말은, 'React에서 판단하기에 가장 적절한 시기' 라는 것이었는데, 이게 도데체 무슨 말인지 도무지 이해가 안갔다.
그런데 프로젝트를 진행하면서, 한 함수에 set- 함수를 여러 개 사용해야 하는 일이 종종 있었다.
그러면서 아래처럼 코드를 작성했다. 코드를 작성하며 생긴 의문들은 아래와 같다.
setTagList(tags)
setAutoCompleteTagList(tags)
"setState를 하면 리렌더링이 되니까, 두번째 줄의 setAutoCompleteTagList는 제대로 동작을 안 하지 않을까?"
"아, 비동기니까 조금 나중에 실행이 되려나? 근데 그렇다고 해도 setAutoCompleteTagList가 제대로 동작할 수 있다는 보장이 있나?"
"왜 setState는 비동기로 동작하는거지?"
아.. 모르겠네 잘 동작하기는 하는데, 찝찝하다. 찾아보자!
그래서 찾아봤다.
사실 우리 말고도 이렇게 고민한 사람들이 이미 예전에 있었고, React를 만드신 Dan 형님이 이번에도 잘 설명해주셨다. 아래의 Issue에 자세히 나와있다. (영어라 몇 번이나 읽었는데도 제대로 이해했는지는 잘 모르겠다^^...)
https://github.com/facebook/react/issues/11527
(내가 해석한) setState가 비동기적으로 작동하는 이유
1. setState가 동기적으로 동작하는데 자주 일어나게되면, 성능적인 문제가 생길 수 있다. 하나의 상태값이 바뀔 때마다 꼬박꼬박 한번씩 리렌더링 하는 것은 효율적이지 않다. set이 여러번 일어난다면, 일괄적으로 set을 묶어 실행하는 편이 더 효율적일 것이다. 이렇게 일괄적으로 변경된 상태 값들을 처리하는 과정을 batch update라고 한다. React에서는 16ms 단위로 batch update를 진행한다. 16ms 동안 변경된 상태 값들을 모아 리렌더링을 진행한다.
2. 내부적인 State, Props, Ref들에 대한 일관성을 보장할 수 있다. 이렇게 일관성을 지킴으로써 Single Source of Truth를 지킬 수 있다. State가 바로 업데이트 된다 하더라도, Props나 Ref가 바로 업데이트된다는 보장은 없기에 만약 새로운 State를 만들 때 이들을 사용한다면 문제가 생길 수 있다.
아래 예시처럼 새로운 State를 만들 때, 아무리 최신의 State를 쓰더라도, 쓰이는 Props가 최신이 아니라면 문제가 생길 수 있다는 것이다.
this.setState({
alarmSet: this.state.alarmTime > 0 && this.props.elapsedTime < this.state.alarmTime
});
그래서 React는 재조정(reconciliation) 그리고 바뀌는 작업(flush) 이후에 State과 Props를 업데이트 하도록 만들어놨다. 만약 이런 일관성을 깨지 않고 업데이트를 하고싶다면, ReactDOM.flushSync(fn)을 사용하면 된다(그런데 이걸 사용하면 전체 리렌더링이 강제적으로 일어나므로, 조심해서 사용해야한다).
3. React의 setState는 실행되는 순서대로 적용되는 것이 보장되어 있다. 그런데 그 setState이 이벤트 핸들러에서 오는지, network response에서 오는지, animation에서 오는지에 따라 우선 순위가 다르다. 예를 들어, 메시지를 입력한다고 하자. 메시지를 타이핑할 때 TextBox 컴포넌트는 빠르게 바뀌어야 하지만, 만약 입력하는동안 새 메시지를 수신하게 되어 새로 렌더링하는 것이 필요하다면, 즉시 새로 렌더링되어 입력에 불편함을 주는 것보다 사용자가 메시지를 입력할 때까지 일정 시간을 기다렸다가 렌더링하는 편이 더 좋을테니까!
4. 성능 문제 뿐만아니라, 사용자의 경험도 향상시켜준다. 만약 render가 일괄적으로 이루어지지않고 개별적으로 이루어진다고 가정했을 때 충분히 빠른 화면 전환이 일어남에도 불구하고 화면 전환 사이에 loading 화면이 계속 들어가게되면 사용자의 경험이 좋지 않게 될 것이다. 비동기 setState는 새로운 State가 업데이트 되는 동안 이전 화면이 보여지게 하고, background에서 State가 업데이트 되게 한다.
🌱 두 번째 의문 - setState 안에 함수를 넣으면 바로 실행이 되네?
Babble 페이지에는 다양한 모달들이 존재한다.
가장 핵심이 되는 채팅 창부터, 사용자에게 notification을 해주어야 하는 것들 대부분이 모달로 표현되어 있다.
우리는 채팅을 한 번에 하나만 열 수 있게 해두었는데, 만약 채팅이 열린 채로 다른 채팅 방을 누르면 '입장 중인 방에서 퇴장을 하시겠습니까?' 라는 메시지를 보여주고, 유저가 확인을 눌러 퇴장을 하고 새 방에 입장하거나, 취소를 눌러 기존 방에 그대로 있게 했다.
이를 구현하는 도중에, set함수에 callback을 함수를 넣어 실행시킬 일이 생겼다.
아래와 같은 코드였는데, confirmCallback이라는 state를 만들고, 그 안에 함수를 넣고, 원하는 곳(clickConfirm)에서 실행되게 만들었다.
const [confirmCallback, setConfirmCallback] = useState(null);
const openModal = (modalInner, callback = null) => {
...
setConfirmCallback(callback);
}
const clickConfirm = () => {
...
confirmCallback?.();
}
...
함수를 state 안에 넣는다는 것이 처음에는 어색했지만, Javascript의 고차함수(Higher Order Function)(함수를 인자로 받거나, 함수를 return하는 함수를 말한다.) 개념을 생각한다면 이상한 일이 아니었기에 이렇게 새롭지만 이렇게 사용했다. 그런데, 여기에서 문제가 발생했다. 분명 openModal에서 아직 실행하지 않은 callback 함수를 set해주었을 뿐인데, 함수가 실행되는 것이었다. 그래서 아래와 같이 다시 한 번 callback으로 감싸 setConfirmCallback을 해주었는데, 제대로 실행되었다.
const [confirmCallback, setConfirmCallback] = useState(null);
const openModal = (modalInner, callback = null) => {
...
setConfirmCallback(() => callback);
}
const clickConfirm = () => {
...
confirmCallback?.();
}
...
분명 실행하지 않은 함수를 넘겨 setState 해주고, 원하는 곳에서 함수를 실행시키려 했는데, 왜 setState 단계에서 함수가 바로 실행이 되어버린걸까?
React의 setState에는 함수를 바로 넣을 수 없기 때문이다.
React의 setState 안에 들어가는 callback 함수는 특별한 의미를 가진다. 생각해보면, 이전 포스팅에서도 잠깐 언급했었다. 이전의 State를 보장하려고 할 때에 setState 안에 callback을 넣어 새로운 state를 반환하는 방식을 사용했다.
React에서는 이렇게 previous State를 받아서 next State를 set 해줄 때, set 함수에 callback 함수를 넣어준다. 그래서 setState에 함수를 전달해주면, React는 지금 들어오는 callback 함수가 어떤 의도로 들어오는 callback 함수인지 판단할 길이 없기 때문에 previous State를 받아 next State를 정해주는 그 callback 함수로 인식을 해버린다. 그렇기 때문에 즉시 실행이 되는 것이다. 이를 해결하기 위해서는 위 코드에서 우리가 했던 방법처럼 익명함수로 한번 더 감싸주면 된다.
마치며
평소에 크게 신경쓰지 않고 사용했던 React의 setState에 대해 조금 더 깊게 고민할 수 있었다.
당최 이해할 수 없는 영어들이 난무해서 이해하는 데 꽤나 오래 걸렸지만 그래도 어느정도 이해하고 납득하면서 왜 이렇게 동작하는지 알아보니 useState를 사용할 때 조금이나마 더 자신있게 사용할 수 있게 된 것 같다. 하하핬!!!
https://reactjs.org/docs/hooks-reference.html#functional-updates
'Woowa Techcourse > Missions' 카테고리의 다른 글
프론트엔드 성능 최적화 (2) | 2021.09.02 |
---|---|
React에서 setInterval 현명하게 사용하기(feat. useInterval) (7) | 2021.08.26 |
npm에 babble만의 라이브러리 올리기 (0) | 2021.08.26 |
브라우저 렌더링 과정을 알아보자! (0) | 2021.08.26 |
React에서의 querySelector, 잘 쓰고 있는 걸까?(feat. Ref) (9) | 2021.08.26 |
댓글