카테고리 없음

인터뷰 질문 useCallback과 useMemo에 차이점

志者必得 2022. 5. 23. 23:21

최근에 회사를 옮기게 되었는데 인터뷰 하다가 나온 질문 중에 하나가 useCallback 과 useMemo을 평소에 사용하는지와 그 차이점은 무엇인지에 대해서 물어보는 것이 였습니다. 저는 평소에 사용하는 것들이라 대답하는 것이 그렇게 어렵지는 않았지만 리액트를 시작하고 어느정도 Optimization을 해보지 않았다면 모를 수도 있는 부분 같아서 한번 정리를 해보려고 합니다.

 

간단한 대답은 useCallback은 함수를 caching하고 useMemo는 결과 값을 caching 한다고 알고 있으면 됩니다.

이해를 돕기 위해 아래 설명을 읽어보시면 이해가 될 것이라 생각됩니다.

 

 

먼저 useCallback과 useMemo에 차이를 알기전에 왜 React 팀에서 이런 React hook을 만들었는 지를 알아야 합니다.

리액트가 re-rendering를 할지 결정할 때 보는 것은 props나 state이 변경 되었는 가를 보게 됩니다. 함수 형태가 prop으로 넘겨져도 똑같습니다.

 

아래 처럼 함수가 다시 만들어지면 ===을 했을 때 false가 나오게 됩니다.

// 곱샘 함수를 리턴하는 함수
const multiply = () => {
	return (a, b) => a*b 
}

const multi1 = multiply()
const multi2 = multiply()

console.log(multi1(2,3), multi2(3,4))

// false
console.log(multi1 === multi2)

useCallback

리액트에서는 자주 함수를 prop으로 넘겨 주게 되는데 아래와 같이 Component가 있으면 List 컴포넌트는 Search가 rerendering을 할 때마다 searchType이 변경 되지 않더라도 계속 re rendering을 해야 합니다. 

왜냐하면 onItemClick이 계속 새로 만들어지기 때문입니다.

function Search({ searchType }) {
  const onItemClick = (item) => {
    console.log(item, searchType)
  }
  return <List onItemClick={onItemClick} searchType={searchType}></List>
}

 useCallback을 사용하면 onItemClick 함수가 계속 새로 만들어 지는 것을 방지 할 수 있습니다.

const onItemClick = useCallback((item) => {
  console.log(item, searchType)
}, [searchType])

위와 같이 useCallback으로 감싸 주고 필요한 dependency를 array 형태로 입력 하면

<List />는 이제 searchType이 변경 되었을 때만 새로운 함수를 만들고 그렇지 않을 경우에는 <Search />가 다시 랜더링을 할 때에도 같은 함수를 cache에서 불러 옵니다.

 

 

useMemo

useMemo는 useCallback과 하는 일이 조금 다릅니다. useMemo는 결과 값을 저장한다고 위에서 말 했습니다. 

어떻때 useMemo를 사용하면 될 까요?

아주 복잡한 계산을 해야 하거나 큰 리스트를 확인 해야 될 때 사용하면 좋습니다.

 

예를 들어서 아래와 같이 2d array에서 위에서 출발해서 아래로 나갈 수 있는지 없는지 알아내는 알고리즘을 컴포넌트에서 실행해야 할 때 pot이 변경 되었을 때만 확인 해보면 됩니다. 랜더링 할 때마다 이 알고리즘을 큰 pot으로 돌려야 한다면 퍼포먼스가 안나올 수 있습니다.

const isWaterFlow = (pot) => {
	// get entry point
  let entryPoint = null
  pot[0].forEach((num, index) => {
  	if(num === 0) entryPoint = [0, index]
  })
  if(!entryPoint) return false

  const queue = []
  
  queue.push(entryPoint)
  
  while(queue.length > 0) {
  	const [r, c] = queue.pop()
    const currentValue = pot[r][c]
    	// set visited
      pot[r][c] = -1
      for(let i = 0; i < 4; i ++) {
      	if(dr[i] > -1 && dc[i] > -1 && currentValue !== -1) {
         const positionValue = pot[r + dr[i]][c + dc[i]]
         	if(r + dr[i] === pot.length - 1) return true
          if(positionValue === 0) {
            queue.push([r + dr[i], c + dc[i]])
            break
          }
        }
       
      }
  }
  return false
}

 

우리는 아래와 같이 복잡한 계산이 필요한 함수를 useMemo로 감싸주고 결과 값을 return 해주면 useMemo 함수가 그 값을 저장해두고 있다가 pot이 변경 될 때만 isWaterFlow을 실행시켜서 memo된 값을 업데이트 해주게 됩니다.

const waterCanFlow = useMemo(() => {
    return isWaterFlow(pot)
  }, [pot])

 

위와 같은 useCallback, useMemo를 알고나면 모든 곳에 사용하고 싶은 마음이 들 수도 있는데 그러면 performance를 더 않 좋게 만들 수 있으니 정말 optimization이 필요한 곳에서 사용을하고 꼭 profile을 해서 변화가 크면 implement를 해주는 것이 좋습니다.