본문 바로가기

FrontEnd

[Epic React] useCallback과 안전한 비동기 훅 만들기

반응형

대충 만든 비동기 훅을 호출하고 렌더링 전 뒤로가기를 누르면 콘솔에 해당 로그가 나온다.

unmount된 컴포넌트에 state update를 시도했기 때문.

가비지 컬렉팅 전, 컴포넌트가 버츄얼 돔 상에 없어도 상태 업데이트를 시도할 수 있다.

해당 문제를 막으면서 useCallback을 공부해보자

 

1. useCallback은 언제 쓰는가.

React.memo 사용 렌더링 최적화시 props로 동일 객체 넘겨주기

의존성 배열에 상태 변화 없음을 알려주기

 

2.  Let's Coding

1. unmount된 컴포넌트의 상태 업데이트를 막는 훅을 보자

주석은 useLayoutEffect를 사용하란 의미다.

(LayoutEffect가 더 먼저 실행된다.)

function useSafeDispatch(dispatch) {
  const mountedRef = React.useRef(false)

  // to make this even more generic you should use the useLayoutEffect hook to
  // make sure that you are correctly setting the mountedRef.current immediately
  // after React updates the DOM. Even though this effect does not interact
  // with the dom another side effect inside a useLayoutEffect which does
  // interact with the dom may depend on the value being set
  React.useEffect(() => {
    mountedRef.current = true
    return () => {
      mountedRef.current = false
    }
  }, [])

  return React.useCallback(
    (...args) => (mountedRef.current ? dispatch(...args) : void 0),
    [dispatch],
  )
}

 

return 콜백은 dispatch 함수의 변경을 허용하고 있다. (의존성 배열)

 

2. useAsync 비동기 함수를 만든다.

unsafeDispatch는 리액트에 의해 immutable이 보장된다.

function useAsync(initialState) {
  const [state, unsafeDispatch] = React.useReducer(asyncReducer, {
    status: 'idle',
    data: null,
    error: null,
    ...initialState,
  })

  const dispatch = useSafeDispatch(unsafeDispatch)

  const {data, error, status} = state

  const run = React.useCallback(
    promise => {
      dispatch({type: 'pending'})
      promise.then(
        data => {
          dispatch({type: 'resolved', data})
        },
        error => {
          dispatch({type: 'rejected', error})
        },
      )
    },
    [dispatch],
  )

  return {
    error,
    status,
    data,
    run,
  }
}

의존성 배열에 eslint-disabled를 쓰는건 좋지 않다.

차라리 클로저를 잘 활용하여 목록을 적게 유지하는게 낫다.

 

3. 다음과 같이 사용한다.

run도 불변이 보장되지만, 의존성 배열에 넣어준 것을 보자.

의존성은 전부 명시적으로 적어주는것이 좋다.

run(dispatch)함수는 reducer의 역할을 하고 있기 때문에,  다형성으로 의존성을 줄인다.

즉 state, reducer 두개만 넣어줘도 대부분 충분한 것이다.

function PokemonInfo({pokemonName}) {
  const {data: pokemon, status, error, run} = useAsync({
    status: pokemonName ? 'pending' : 'idle',
  })

  React.useEffect(() => {
    if (!pokemonName) {
      return
    }
    run(fetchPokemon(pokemonName))
  }, [pokemonName, run])

  if (status === 'idle') {
    return 'Submit a pokemon'
  } else if (status === 'pending') {
    return <PokemonInfoFallback name={pokemonName} />
  } else if (status === 'rejected') {
    throw error
  } else if (status === 'resolved') {
    return <PokemonDataView pokemon={pokemon} />
  }

  throw new Error('This should be impossible')
}

 

심화학습은 아래에서

When to useMemo and useCallback (kentcdodds.com)

 

When to useMemo and useCallback

Performance optimizations ALWAYS come with a cost but do NOT always come with a benefit. Let's talk about the costs and benefits of useMemo and useCallback.

kentcdodds.com

 

반응형