momo
7 min readJul 5, 2022

--

React 컴포넌트 최적화 : memoization 2탄 ‘useCallback’

Memoization :

자주 사용되는 값을 받아오기 위해 반복적으로 계산을 해야한다면, 이전에 이미 계산한 값을 캐싱하여 해당 값이 필요할 때마다 반복적인 계산이 아닌, 이를 메모리에서 꺼내 사용하는 최적화 기법.

돌아보는 useMemo

‘useMemo’ 를 통한 캐싱 FLOW :

인자로 콜백함수를 넣어주면 이 함수가 리턴하는 값을 memoization.

useMemo를 통해 캐싱하여 재사용 ( 리턴값을 캐싱 )

useCallback : 인자로 전달한 콜백함수 그 자체를 memoization

useCallback을 활용한 memoization ( 함수를 캐싱 )

흔히 함수를 정의할 때, 자주 사용하는 함수를 재사용하고싶다면 이를 useCallback으로 감싸주어 사용해봅시다.

React에서 함수형 컴포넌트의 memoization을 이해하기 위해 필요한 지식

  • javascript의 함수는 ‘객체'의 한 종류입니다.
  • react에서의 함수형 컴포넌트는 말 그대로 ‘함수'이기 때문에, 함수형 컴포넌트가 반복적으로 렌더링이 된다는 뜻은 함수가 반복적으로 호출된다고 생각해야 합니다.
  • 함수형 컴포넌트가 새로 호출되는 시점에선 컴포넌트 내부 변수는 초기화 과정을 거치게 됩니다.

예시 코드로 이해해봅시다.

위의 함수형 컴포넌트는 calculate라는 함수를 가지고 있습니다. 이는 calculate라는 변수 안에 함수 객체가 할당된 형태입니다. 해당 함수형 컴포넌트는 렌더링이 되는 시점마다 calculate 변수가 초기화 되고, 새로 만들어진 함수 객체를 다시 할당받게 됩니다. ( 리액트에서 렌더링은 state가 업데이트 됨에 따라 필연적으로 일어나는 일인 것도 주의깊게 보셔야 합니다.)

자 그럼 이를 어떻게 해결해보면 좋을까요?

calculate라는 함수를 useCallback으로 감싸주면 해결됩니다. memoization을 활용하는 것이죠. 이렇게 되면 component가 re-rendering되더라도 초기화 되는 것을 막을 수 있습니다. 첫 렌더링 시점에서 이 함수 객체를 만들어 초기화하고 이후 렌더링 시점에서는 이를 막는 기법이죠.

개념은 이해하셨을 거라 생각되니 문서를 보며 더 깊게 알아보시죠.

여기서 fn은 memoization할 콜백함수, deps는 의존성배열을 나타냅니다.

결과적으로 이렇게 나타낼 수 있겠죠..? 이렇게 함수를 useCallback으로 감싸주면 calculate라는 변수는 memoization된 함수를 갖고 있게 됩니다. 이렇게 memoization된 calculate함수는 의존성 배열 내부에 있는 값이 변경되지 않는 이상 다시 초기화 되지 않습니다. 만약 의존성배열에 명시해준 값이 변경이 되는 경우는 re-rendering이 되면 새로 만들어진 함수 객체로 초기화되는 과정을 거치게 됩니다.

여기까지 잘 따라오셨다면, 이제 실제 리액트 구조를 보며 이해해보도록 합시다.

위의 코드는 간단합니다. number라는 값을 state로 지정하고 input tag로 숫자를 받아 이를 setNumber로 업데이트합니다. state가 업데이트 됨에 따라 re-rendering이 일어나고 이후 버튼을 누르면 해당 state를 콘솔에 찍어주겠죠..?

자 그렇다면 이제 componentDidmount 시점에서의 useEffect를 통해 memoization을 구체화해보도록 합시다.

해당 코드는 렌더링 시점에서 someFunction이 변경되는 경우에만 로그가 찍히게 하기 위해 작성한 코드입니다. 하지만 결과를 출력해보면 버튼을 클릭하지 않아도 input tag 안의 value가 바뀜에 따라 호출되는 것을 확인할 수 있습니다. 왜 그럴까요?

React component는 state가 업데이트가 됨에 따라 re-rendering이 발생하기 때문인데요. App이라는 함수는 state가 업데이트 됨에 따라 내부 변수도 자연스레 초기화되기 때문입니다. 때문에 App안의 someFunction도 초기화가 이루어지고 자연스레 호출되는 것이죠.

또한 js에서 변수에 할당되는 객체는 값을 저장하는 것이 아닌 메모리 공간 안의 객체를 참조하고 있습니다. 즉 메모리 공간의 주소값을 저장한다고 이해해야합니다.

함수 역시 객체이기 때문에 someFunction이라는 변수 안에는 해당 함수(객체)의 주소값이 저장되어 있는 것입니다.

따라서 결과적으로

state 업데이트 → re-rendering → 함수형 컴포넌트 안의 변수 초기화 → someFunction은 객체(함수)의 주소값을 할당 → 렌더링 시점에서 주소값 변경 ( 다시 함수 객체가 생성되고 다른 메모리 공간에 저장 ) → someFunction이라는 변수 안에는 이전과는 다른 메모리 주소가 할당

따라서 someFunction의 주소값이 달라짐에 따라 useEffect의 의존성배열인 someFunction에 영향을 받게 되는 것이죠.

자 이제 useCallback을 사용해 렌더링이 되더라도 someFunction이 바뀌지않게 만들어봅시다.

해당 코드는 더이상 state (number)가 업데이트 되더라도 useEffect의 콜백함수가 실행되지 않음을 나타내고 있습니다.

하지만 useCallback의 2번째 인자인 의존성배열에 빈 배열을 넣어주었기 때문에 더이상 버튼을 눌러도 해당 state(number)값이 로그에 찍히지 않습니다.

따라서 다음과 같이 number가 바뀌는 경우 새로운 memoization을 위해 의존성배열에 state값을 넣어 해결해야합니다.

오늘은 useCallback에 대해서 개념부터 활용까지 알아보았습니다.

핵심은 비즈니스 로직에서 하나의 함수형 컴포넌트 안에 많은 객체(함수)가 할당되어 있을 때에 유용하다는 점입니다. re-rendering시점에서 굳이 같은 함수를 초기화하고 재할당하는 경우가 많이 생긴다면 퍼포먼스 이슈에 치명적이기 때문이죠.

다음 주제는 조금 생소할법한 useReducer라는 훅에 대해 이야기해보려고 합니다. 생소할 개념이라 생각하고 좀더 깊게 정리해볼게요! 곧 만나요~~

--

--