useState setter 함수의 콜백함수 내부에서 값 업데이트

렐루·2024년 7월 25일
0

리액트

목록 보기
19/20

1. useState setter 함수 업데이트가 비동기적으로 동작

const handleClickDayGroupType = (text: "주중" | "전체") => {
    let weekdayResult
    let wholedayResult
    switch (text) {
      case "주중":
        setDayGroupType((prev) => {
          if (prev === text) {
            weekdayResult = false
            return ""
          } else {
            weekdayResult = true
            return text
          }
        })
        console.log("weekdayResult :", weekdayResult)
        if (weekdayResult) {
          // 주말 결과 하드코딩 ㅎㅎ
          setDayChecks([true, true, true, true, true, false, false])
        } else {
          setDayChecks(WEEK_DAY_LIST.map(() => false))
        }
        break
      case "전체":
        setDayGroupType((prev) => {
          if (prev === text) {
            wholedayResult = false
            return ""
          } else {
            wholedayResult = true
            return text
          }
        })
        console.log("wholedayResult :", wholedayResult)
        if (wholedayResult) {
          console.log("전체", wholedayResult)
          setDayChecks(WEEK_DAY_LIST.map(() => true))
        } else {
          console.log("전체", wholedayResult)
          setDayChecks(WEEK_DAY_LIST.map(() => false))
        }
        break
    }
  }

위의 코드는 클릭했을시 값이 업데이트 되는데 state의 가장 최신값으로 업데이트 하고 싶어서 setter 함수 내부에서 값을 받아서 setter 함수 밖에서 다른 값을 업데이트하는 로직입니다.

if (weekdayResult) {
	// 주말 결과 하드코딩 ㅎㅎ
  setDayChecks([true, true, true, true, true, false, false])
} else {
  setDayChecks(WEEK_DAY_LIST.map(() => false))
}

weekdayResult가 if 문에서 업데이트 되지 않아서 처음에 당황했는데 setter의 내부 로직이 동기적으로 작동한다고 착각하고 있었습니다 ㅎㅎ


2. 해결 방법

위의 오류를 해결하는 방법에 대해 말씀드리겠습니다.

2-1. useEffect 사용하기

const handleClickDayGroupType = (text: "주중" | "전체") => {
  switch (text) {
    case "주중": {
      setDayGroupType((prev) => (prev === text ? "" : text))
      break
    }
    case "전체": {
      setDayGroupType((prev) => (prev === text ? "" : text))
      break
    }
  }
}

useEffect(() => {
  if (dayGroupType === "주중") {
    setDayChecks([true, true, true, true, true, false, false])
  } else if (dayGroupType === "전체") {
    setDayChecks(WEEK_DAY_LIST.map(() => true))
  } else {
    setDayChecks(WEEK_DAY_LIST.map(() => false))
  }
}, [dayGroupType])

위의 방법은 setDayGroupType가 업데이트 되면 해당 업데이트를 바탕으로 업데이트를 진행하는 방법입니다.

2-2. 직접 업데이트 하기

const handleClickDayGroupType = (text: "주중" | "전체") => {
  if (text === "주중") {
    if (dayGroupType === "주중") {
      setDayGroupType("")
      setDayChecks(WEEK_DAY_LIST.map(() => false))
    } else {
      setDayGroupType("주중")
      setDayChecks([true, true, true, true, true, false, false])
    }
  } else if (text === "전체") {
    if (dayGroupType === "전체") {
      setDayGroupType("")
      setDayChecks(WEEK_DAY_LIST.map(() => false))
    } else {
      setDayGroupType("전체")
      setDayChecks(WEEK_DAY_LIST.map(() => true))
    }
  }
}

위의 로직은 선행되어야하는 상태의 값이 단순하고 다음 업데이트의 값이 정확하게 예측되기 때문에 해당 값의 업데이트를 가정하고 작성한 방법입니다.
단점으로는 동기가 여러번에 걸쳐 진행되며 중간단계일 경우에는 불가능한 방법이지만 위의 useEffect 방법보다는 현 상황에서는 좋은 방법인 것 같습니다.


3. useRef 사용하기 (매력적인 오답!)

 const weekdayResult = useRef(false)
 const wholedayResult = useRef(false)

 const handleClickDayGroupType = (text: "주중" | "전체") => {
   switch (text) {
     case "주중":
       setDayGroupType((prev) => {
         if (prev === text) {
           weekdayResult.current = false
           console.log("주중 :", weekdayResult.current)
           return ""
         } else {
           weekdayResult.current = true
           console.log("주중 :", weekdayResult.current)
           return text
         }
       })
       console.log("weekdayResult.current :", weekdayResult.current)
       if (weekdayResult.current) {
         // 주말 결과 하드코딩 ㅎㅎ
         setDayChecks([true, true, true, true, true, false, false])
       } else {
         setDayChecks(WEEK_DAY_LIST.map(() => false))
       }
       break
     case "전체":
       setDayGroupType((prev) => {
         if (prev === text) {
           wholedayResult.current = false
           return ""
         } else {
           wholedayResult.current = true
           return text
         }
       })
       console.log("wholedayResult.current :", wholedayResult.current)
       if (wholedayResult.current) {
         setDayChecks(WEEK_DAY_LIST.map(() => true))
       } else {
         setDayChecks(WEEK_DAY_LIST.map(() => false))
       }
       break
   }
 }

위의 방법은 useRef가 랜더링 업데이트와는 무관하게 즉시 업데이트 된다는 특성을 이용한 방법입니다.
저는 위의 방법이 가능할 것이라고 생각했는데 매우 잘못되었습니다.

위의 코드의 함수 handleClickDayGroupType를 실행한 이미지를 보여드리겠습니다.

첫번째는 주중이 먼저 찍히지만 두번째부터는
weekdayResult.current가 먼저 찍히게 됩니다.

이는 위의 코드에서
setter 함수 내부의 콘솔 console.log("주중 :", weekdayResult.current) 보다
setter 함수 외부 아래의 콘솔 console.log("weekdayResult.current :", weekdayResult.current)가 먼저 찍히게 된다는 뜻입니다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false,
}

export default nextConfig

위의 주중이 두번 찍히는 문제는 리액트 strickmode를 off 하면 해결됩니다.

위의 오류과정을 통해 리액트 setter 함수의 업데이트가 비동기적으로 동작한다는 의미를 이해할 수 있었습니다.


4. 가장 좋아보이는 해결책

가장 좋은 해결책은 상태를 줄이고 최대한 stter 함수와 state를 이용해서 값을 계산하는 것입니다!!

const handleClickDayGroupType = (text: "주중" | "전체") => {
  const selectedDayLength = dayChecks.filter((dayCheck) => dayCheck).length
  switch (text) {
    case "주중":
      if (selectedDayLength === 5 && !dayChecks[0] && !dayChecks[6]) {
        return setDayChecks(WEEK_DAY_LIST.map((_) => false))
      } else {
        return setDayChecks([false, true, true, true, true, true, false])
      }
    case "전체":
      if (selectedDayLength === 7) {
        return setDayChecks(WEEK_DAY_LIST.map((_) => false))
      } else {
        return setDayChecks(WEEK_DAY_LIST.map((_) => true))
      }
  }
}

상태가 너무 복잡해져서 잠깐 손을 내려놓고 어떻게 코드를 줄일까 고민을 했습니다.
생각해보니 모든 값이 같은 상태를 기반으로 계산되고 있었습니다.
그레서 모든 추가 상태를 삭제하고 기반이 되는 상태의 setter 함수를 이용하여 모든 값을 계산했습니다.

앞으로 상태를 추가로 생성하기 전에 원하는 값이 이전 state로 참조되는지 두번세번 확인하고 생성해야겠습니다.

긴글 읽어주셔서 감사합니다!

profile
프론트 공부중입니다!

0개의 댓글