State 업데이트가 반영이 즉시 안되는 이유

최환석·2023년 6월 28일
1

서론

오늘도 평화롭게 개발하는 최비모씨는 열심히 리액트로 코드를 짜고 있다. 그가 맡은 파트는 바로 드래그 앤 드랍으로 상태를 변경하는 컴포넌트를 구현을 하고 있다. 그가 작성한 코드는 아래와 같다.

function FormLayout() {  
const { data, refetch } = useQuery(['form'], getAdminFormList);  
const [firstPageData, setFirstPageData] = useState<AdminForm[]>([]); 
const [secPageData, setSecPageData] = useState<AdminForm[]>([]);  
  
/// drag가 끝나면 호출되는 함수
const onDragEnd = (result: DropResult) => {  
const { source, destination } = result;  
  
if (!destination) return;  
  
if (destination.droppableId === source.droppableId) {  
handleSomeBoardMove(source, destination);  /// 밸류를 state를 사용하여 변형 하는 로직
} else {  
handleDifferentBoardMove(source, destination);  /// 밸류를 업데이터 하는 로직
}  
};  
  

먼저 reactQuery를 이용해 데이터를 가져온 다음에 그것을 보여준다. 만약 드래그가 끝나면 드래그 밸류를 업데이트 하여 새로 갱신하여 보여준다.

하지만 여기서 문제가 발생했다. 드래그까지는 구현을 하였지만 이 상태를 서버에 업로드를 하고 싶어서 해당하는 상태를 불러오기로 했다. 하지만 해당하는 함수 스코프(함수가 실행되는 맥락)에서는 아무리 firstPage를 불러와도 업데이트 되지 않았던 것 이다! 기묘하지 않은가 분명 보여주는 것은 제대로 보여줬다고 판단이 되지만 해당하는 함수는 업데이트가 안되었다니! 사실 이는 React에서 useState가 작동되는 방식을 알게 되면 당연한 일이다.

본론

자 앞서서 아래의 코드를 살펴보자

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

아래의 코드는 한번의 클릭으로 setNumber를 3번 호출하여 한번에 number를 3을 올리고 싶다. 하지만, 의도치 않게 동작을 할 것 이다.

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1); // 1
        setNumber(number + 1); // 1
        setNumber(number + 1); // 1
      }}>+3</button>
    </>
  )
}

실제 실행 결과는 다음과 같을 것이다. 왜냐? state render가 갱신될때 마다 값이 업데이트 되기 때문이다. 즉 해당하는 요소가 바뀌어야 number가 바뀌게 된다. 즉 state의 업데이트는 렌더링이 끝난 직후 업데이트가 일어난다. 그렇기 때문에 위의 실행 결과는 다음과 같을 것 이다.

 setNumber(0 + 1); // 1
 setNumber(0 + 1); // 1
 setNumber(0 + 1); // 1

즉 로직이 onClick의 콜백 함수 안에 있는 로직을 전부 실행시키고 state를 갱신 시키기 때문이다. 이렇게 된 이유는 조금만 생각해보면 간단하다.
만약에 setNumber가 업데이트 될때마다 render를 유발하게 되면 무수히 많은 렌더링을 일으키기 때문이다. 3번이여서 괜찮지 100개 1000개 라면 ..?

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>
    </>
  )

비동기 처리를 하여도 마찬가지 이다. 로직이 실행될때 이렇게 이야기 할 것 이다.
0이라는 값을 3000ms 뒤에 실행시켜줘 라고

그렇다면 이를 해결하기 위해서는 어떻게 해야할까?

그래서 어떻게 해결하죠?

먼저 카운터의 경우를 생각해보자. 만약에 내가 setNumber를 3번 실행시켜서 각각 +1 씩 해주고 싶다면 아래 와 같이 실행하면 될 것 이다.

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber((prev) => prev + 1);
        setNumber((prev) => prev + 1);
        setNumber((prev) => prev + 1);
      }}>+3</button>
    </>
  )
}

set 함수에 호출할때 콜백 함수를 넣어줄 경우 첫번째 파라미터로 이전의 값을 그대로 준다. 그걸 응용하여 새로운 값을 return 해줄 경우 그 값으로 적용이 된다.

위의 실행 결과는 다음과 같다.

setNumber((0) => 0 + 1);
setNumber((1) => 1 + 1);
setNumber((2) => 2 + 1);

원점으로 돌아와서

자 그럼 다시 원점으로 돌아와서 드래그 앤 드롭을 핸들링 해보자. 먼저 시나리오는 다음과 같다.

하지만 위의 내용으로 보면 실제 react에서 작동되는 로직을 고려하여 시나리오를 짜면 다음과 같이 짜면 된다.

function FormLayout() {  
const { data, refetch } = useQuery(['form'], getAdminFormList);  
const [firstPageData, setFirstPageData] = useState<AdminForm[]>([]); 
const [secPageData, setSecPageData] = useState<AdminForm[]>([]);  
const [isDragEnd, setIsDragEnd] = useState(false);
  
/// drag가 끝나면 호출되는 함수
const onDragEnd = (result: DropResult) => {  
const { source, destination } = result;  
  
if (!destination) return;  
  
if (destination.droppableId === source.droppableId) {  
handleSomeBoardMove(source, destination);  /// 밸류를 state를 사용하여 변형 하는 로직
     setIsDragEnd(true);
} else {  
handleDifferentBoardMove(source, destination);  /// 밸류를 업데이터 하는 로직
    setIsDragEnd(true);
}  
useEffect(() => {
    if (isDragEnd) {
      updateOrder(); /// 순서를 변경하여 서버에 post 하는 메소드
    }
  }, [isDragEnd, firstPageData, secPageData]);
  
}

그렇게 그는 행복하게 코딩을 했다고 합니다!

마무리

react를 하면서 느낀 것 이지만, react의 편안함 사이에는 js의 탄탄한 기본기가 있어야 하며 또 컴퓨터 적으로 사고를 하며 닥친 문제 상황을 순차적으로 이해 하여 풀어가는 것이 중요하다고 느꼈다.

참고한 레퍼런스
https://react.dev/learn/state-as-a-snapshot

profile
항상 즐겁고 재밌게!

0개의 댓글