[WEB] Reack Hooks API : UseMemo 쉽게 이해하기
React를 처음 시작하려면 클래스 단위의 컴포넌트와 리액트 라이프 사이클에 대한 이해가 반드시 필요합니다. 하지만 리액트 16.8 버전 이후 hook을 대중적으로 사용하게 되면서 보다 편리하고 직관적인 개발이 가능하게 되었죠.
state관리는 리액트에서 굉장히 중요합니다.
상대적으로 친근한 useState, useEffect와 같은 API는 다루지 않고, 오늘은 useMemo에 대해 다뤄보려고합니다.
useMemo
useMemo는 memoization에 대한 이해가 우선적으로 필요합니다. 알고리즘을 공부하다 보면 자주 접하는 개념이지만 생소하다면 꼭 관련 자료들을 참고해 이해하면 좋을 것 같습니다. 기본적인 컨셉은 재사용이며, 동일한 인풋에 대해 아웃풋이 동일하다면 이를 저장해두고, 동일한 입력에 대응하는 방식입니다. 쉽게 말하자면, 자주 필요한 값을 캐싱하여 캐시에서 꺼내 사용하는 플로우입니다.
리액트의 state가 업데이트되면, 관련 컴포넌트의 re-rendering은 불가피합니다.페이지가 1개의 컴포넌트라면 1번의 렌더링이 일어나겠지만, Atomic과 같은 디자인 패턴을 적용한 경우라면.. 수십가지의 컴포넌트로 페이지가 구성되어 있을 것이고 이는 크리티컬한 성능 저하와 더불어 극악의 사용자 경험을 제공하게 될 것입니다. 그렇다면 어떻게 문제를 해결할 수 있을까요?
오늘 살펴볼 useMemo가 바로 이와 같은 고민을 어느정도 해소해 줄 수 있을 겁니다.
코드를 한번 살펴보시죠!
MainComponent에서 HeaderContainer에 props로 x,y의 값을 넘기고, HeaderContainer는 이를 받아 엄청나게 복잡한 hashAlgorithm의 argument에 주입합니다. 그리고 이에 대한 결과를 화면에 출력한다고 가정해보시죠.
이러한 경우는 사실 극단적이지만, 위와 같은 경우라면 복잡한 알고리즘이 렌더링시마다 호출되며 사용자는 굉장히 불편한 경험을 지속적으로 하게 될 것입니다. 그렇다면 함수형 컴포넌트에 memoization을 적용해보면 어떨까요? 함수의 인자로 넘어오는 값이 항상 바뀌는 경우가 아니라면 굳이 해당 함수를 렌더시마다 호출할 필요는 없을 것입니다. 렌더링 시점에서 이전 렌더링과 현재 렌더링 간의 x, y 와 같은 인자를 비교하고 동일한 경우 다시 함수를 호출하지않고 메모리 상 저장해 두었던 result값을 그대로 사용한다면..?
useMemo에 대한 공식 문서의 설명을 살펴보시죠.
메모이제이션된 값을 반환합니다.
“생성(create)” 함수와 그것의 의존성 값의 배열을 전달하세요.
useMemo
는 의존성이 변경되었을 때에만 메모이제이션된 값만 다시 계산 할 것입니다. 이 최적화는 모든 렌더링 시의 고비용 계산을 방지하게 해 줍니다.
useMemo
로 전달된 함수는 렌더링 중에 실행된다는 것을 기억하세요. 통상적으로 렌더링 중에는 하지 않는 것을 이 함수 내에서 하지 마세요. 예를 들어, 사이드 이펙트(side effects)는useEffect
에서 하는 일이지useMemo
에서 하는 일이 아닙니다.배열이 없는 경우 매 렌더링 때마다 새 값을 계산하게 될 것입니다.
무분별한 사용에 대한 경고 또한 잊지않았네요.
useMemo
는 성능 최적화를 위해 사용할 수는 있지만 의미상으로 보장이 있다고 생각하지는 마세요. 가까운 미래에 React에서는, 이전 메모이제이션된 값들의 일부를 “잊어버리고” 다음 렌더링 시에 그것들을 재계산하는 방향을 택할지도 모르겠습니다. 예를 들면, 오프스크린 컴포넌트의 메모리를 해제하는 등이 있을 수 있습니다. useMemo
를 사용하지 않고도 동작할 수 있도록 코드를 작성하고 그것을 추가하여 성능을 최적화하세요.
간단히 말하자면 useMemo 함수는 2개의 인자를 받아 동작하게 됩니다.
첫번째는 결과값을 생성해주는 함수, 두번째는 기존 결과값에 대한 의존성을 보장해주는 입력값의 배열입니다.
만약 두번째 의존성 배열이 빈 배열인 경우, 맨 처음 컴포넌트가 마운트 되었을 때만 이후에는 항상 memoization된 값을 사용하게 됩니다.
컨셉에 대한 이해는 이정도로 하시고, 빠르게 코드로 실습해보도록 하시죠!
state로 지정한 Number는 렌더시 Calculator라는 함수에 담겨 sum이라는 결과값을 받게 됩니다. input tag 이벤트에 setNumber를 통해 state를 업데이트 시키고 이는 자연스레 re-rendering으로 이어지게 될 것입니다.
위 코드를 실행해보면 실제로 상당히 버벅거리는 것을 알 수 있습니다. Calculator는 약 10억번의 연산 이후 number+ 10000값을 리턴하기 때문이죠.
위 코드를 다시 살펴보죠. easyCalculator라는 함수를 만들었고 이는 반복문을 거치지 않는 아주 단순한 함수입니다. 마찬가지로 실행해 살펴보면, 역시나 딜레이가 걸리는 모습을 확인해 볼 수 있습니다. 왜 그럴까요?
이는 state가 업데이트 되면서 발생하는 rendering 때문입니다. 함수형 컴포넌트 안에서 Calculator함수든, easyCalculator함수든 rendering되는 시점에 실행되어 영향 받기 때문인데요. 이러한 상황을 해결하기 위해 우리는 오늘 배운 useMemo를 활용할 수 있겠습니다.
useMemo를 호출한다면 반복문을 사용한 Calculator함수를 memoization을 통해 easyCalculator를 딜레이 없이 호출할 수 있게 됩니다.
조금 감이 오시나요? 좀더 실무와 관련된 예제를 통해 확실히 알아보도록 하죠!
해당 컴포넌트는 number, isKorea라는 state를 가지고 있고, useEffect의 두번째 인자 의존성 배열에 location을 담아 location이 바뀌는 경우에만 useEffect가 호출되도록 생성되었습니다. 즉 아무리 number가 업데이트 되어도 useEffect는 호출되지 않게 됩니다. 하지만 만약에.. location이 string과 같은 primitive type이 아닌 objects라면..?
location 객체의 country라는 key값에 조금 전 삼항 조건 연산자 코드를 그대로 적용했습니다. 객체로 바꿨을 뿐인데 왜 number가 바뀌면 useEffect가 호출되는 것일까요?
객체 타입은 변수에 직접적으로 할당되는 것이 아닌 메모리상에 저장되고 변수에는 해당 메모리 공간에 대한 주소만 할당되는 것이기 때문입니다.
때문에 대표적으로 같은 key와 value 쌍을가진 object를 비교연산자로 나타내보면 false가 출력되는 것을 쉽게 확인하실 수 있습니다. (메모리 주소가 다르기 때문이지요)
따라서 위와 같은 상황도 이해가 되기 시작합니다. useEffect시 두번째 인자의 배열에 location을 담아 이를 주시하게 되는데, object의 경우 메모리 주소값이 달라지기 때문에 같은 key와 value쌍을 갖더라도 이는 분명 다른 값으로 인식된다는 것이지요.
본론으로 돌아와, 이 경우 useMemo가 아주 쓰임새있게 활용될 수 있습니다.
렌더 시점에서 location 변수가 초기화 되는 것을 막기 위해 useMemo를 사용해보겠습니다.
memoization을 통해 object를 처리하고 컴포넌트를 최적화한 흐름을 이해하셨나요? 사실 React Component 단에서 Calculator예제처럼 1초 이상 걸리는 로직으로 초기화를 해야하는 작업은 흔하지 않습니다. 때문에 2번째 object 타입에서의 useMemo의 활용을 꼭 이해하시면 개발에 도움이 많이 되실 것 같네요. 오늘도 긴 글 읽어주셔서 감사합니다!
다음은 컴포넌트 최적화를 위한 두번째 방법 useCallback에 대해 알아보도록 하겠습니다~ 곧 다시 만나요 :)