useMemo도 useEffect와 비슷한 부분이 많습니다. state가 있는 컴포넌트에 state 변화가 생기면 재평가 후 새로 랜더링을 하기때문이죠. 어떤부분이 다른지 왜 이 두가지로 분리했는지는 뒤에서 차근차근 살펴보겠습니다.
아래 코드를 살펴봅시다. 현재 값이 변하는 곳은 총 3군데입니다.
- 이름을 입력할 때 → setName
- 아이디를 입력할 때 → setId
- 버튼을 눌렀을 때 → setUserInfo + getNum
리액트는 상태값이 바뀔 때마다 컴포넌트를 새로 렌더링하기 때문에 3가지 경우 모두 렌더링됩니다. 렌더링이 되는 곳마다 console.log()를 실행해 볼까요?
import { useState, useRef } from "react"; const App = () => { const inputName = useRef(null); const inputId = useRef(null); const [userInfo, setUserInfo] = useState([]); const [name, setName] = useState(""); const [id, setId] = useState(""); const getNum = (li) => { console.log("렌더링"); return li.length; }; const handleSubmit = (e) => { e.preventDefault(); // userInfo.push({}); const newInfo = [...userInfo, { name: name, id: id }]; inputName.current.value = ""; inputId.current.value = ""; inputName.current.focus(); setUserInfo(newInfo); // onChange 이벤드가 발생될 때마다 상태값 변경됨 console.log("렌더링-3"); }; const handleInputName = (e) => { setName(e.target.value); // onChange 이벤드가 발생될 때마다 상태값 변경됨 console.log("렌더링-1"); }; const handleInputId = (e) => { setId(e.target.value); // onChange 이벤드가 발생될 때마다 상태값 변경됨 console.log("렌더링-2"); }; return ( <> <form> <input type="text" placeholder="이름을 입력하세요" onChange={handleInputName} ref={inputName} /> <input type="text" placeholder="아이디를 입력하세요" onChange={handleInputId} ref={inputId} /> <button type="submit" onClick={handleSubmit}> 버튼 </button> </form> <ul> {userInfo.map((value, index) => ( <li key={index}> <h3>{value.name}</h3> <strong>@{value.id}</strong> </li> ))} </ul> <span>{getNum(userInfo)}</span> </> ); }; export default App;
처음 컴포넌트가 실행됬을 때 전체 렌더링이 일어난 후에, 이름을 입력하는 input창에 3번 입력을 했을 때 렌더링-1, 렌더링이 출력되었습니다.


다시 새로고침하고 확인해볼까요? 처음 컴포넌트가 실행됬을 때 전체 렌더링이 일어난 후에, 아이디를 입력하는 input창에 3번 입력을 했을 때 렌더링-2, 렌더링이 출력되었습니다.


입력할 때는 입력 렌더링이, 버튼을 누를 때는 버튼 렌더링과 getNum 렌더링이, 아이디를 입력할 때는 아이디 렌더링이 출력되어야 합니다.
하지만 실행해보면 다르게 출력되는 것을 확인할 수 있어요! 아이디를 입력하는데도, getNum에 있는 렌더링이 함께 출력되고 있습니다.
현재 바뀌는 부분은 input 창입니다. 그렇다면 React는 바뀌는 부분만 렌더링을 해야하는데 코드 전체도 렌더링되고 있습니다.
리액트에서는 가상돔이라는것을 사용하는데요 이것은 기존 돔과는 다르게 엄청 가벼운 정보만 가지고있어서 빠르게 비교가 가능합니다.
브라우저에서 쿼리셀렉터등을 이용하여 돔선택을하면 매우많은 정보들이 들어있는데 그것들을 모두 가지고 비교하기에는 너무 무겁고 느립니다!
그래서 비교에 필요없는 부분들은 빼버린 가상돔을 이용해 실제 비교에 필요한 부분들만 비교합니다.
최초렌더링 -> 이전 렌더링부분에 가상돔 저장 -> 상태업데이트 -> 상태업데이트로인한 가상돔렌더링 -> 업데이트로인해 렌더링된 가상돔과 이전에 만들어진 가상돔을 비교 -> 바뀐부분 브라우저에 렌더링
위와같은 과정으로 이루어지는데요.
아래에서 getNum이 실행되는 이유는
1. return 에서 결과를 내보낸다 브라우저에 렌더링전에 가상돔으로 렌더링(생성)<< 이때 getNum이 실행됨
2. 이전에 렌더링했던 가상돔과 업데이트될예정인 가상돔을 비교(상태변화가 일어났으니 어떤부분이 바뀐건지 확인해야함)
3. (2) 를 통해 브라우저에 렌더링 할 부분 검색
4, 브라우저에 렌더링
이런 과정으로 실행되기 때문입니다!
userMemo 사용해보기
useMemo를 사용해서 컴포넌트 내부 연산을 최적화해봅시다.
const memoizedValue = useMemo(() => 최적화할함수(a, b), [a, b]);
최적화 할 함수와 함수가 의존하게 될 값을 useMemo에게 전달하면 됩니다. useMemo는 렌더링 중에 실행되어서 의존하는 값이 변경이 되었는지를 체크하고, 의존성 값이 변경되었을 때에만 값을 다시 계산합니다.
메모이제이션 - 이전의 값을 기억합니다.
우리는 input에 값을 입력할 때 userInfo는 아직 바뀌지 않기 때문에 userInfo의 값을 기억해줄게요. useMemo를 사용하실 때는 앞에 useMemo를 쓰시고 그 안에 화살표 함수로 최적화할 함수를 넣어줍니다. 두번째 인자로를 최적화할 함수가 의존하는 값을 배열형태로 넣어주면 됩니다. 이렇게 되면 useInfo 값에 의존하는 getNum()함수가 useInfo의 값이 바뀔 때만 실행됩니다.
const n = useMemo(() => getNum(userInfo), [userInfo]);
다시 값을 입력해보세요!


import { useState, useRef, useMemo } from "react"; const App = () => { const inputName = useRef(null); const inputId = useRef(null); const [userInfo, setUserInfo] = useState([]); const [name, setName] = useState(""); const [id, setId] = useState(""); const getNum = (li) => { console.log("렌더링"); return li.length; }; const n = useMemo(() => getNum(userInfo), [userInfo]); const handleSubmit = (e) => { e.preventDefault(); const newInfo = [...userInfo, { name: name, id: id }]; inputName.current.value = ""; inputId.current.value = ""; inputName.current.focus(); setUserInfo(newInfo); // onSubmit 이벤트가 발생될 때마다 상태값 변경됨 console.log("렌더링-3"); }; const handleInputName = (e) => { setName(e.target.value); // onChange 이벤트가 발생될 때마다 상태값 변경됨 console.log("렌더링-1"); }; const handleInputId = (e) => { setId(e.target.value); // onChange 이벤트가 발생될 때마다 상태값 변경됨 console.log("렌더링-2"); }; return ( <> <form> <input type="text" placeholder="이름을 입력하세요" onChange={handleInputName} ref={inputName} /> <input type="text" placeholder="아이디를 입력하세요" onChange={handleInputId} ref={inputId} /> <button type="submit" onClick={handleSubmit}> 버튼 </button> </form> <ul> {userInfo.map((value, index) => ( <li key={index}> <h3>{value.name}</h3> <strong>@{value.id}</strong> </li> ))} </ul> <span>{n}</span> </> ); }; export default App;
Mini Question!
button의 클릭 이벤트에서 새로운 array를 만들어서 새로 setUserInfo를 해주는 이유가 뭘까요?
아래와 같이 push하면 페이지가 왜 렌더링되지 않을까요?
<button type="submit" onClick={(e) => { e.preventDefault(); userInfo.push({name:inputName.current.value, id: inputId.current.value}); setUserInfo(userInfo); inputName.current.value = ""; inputId.current.value = ""; inputName.current.focus(); console.log("렌더링중-3-"); }} > 버튼 </button>
정답
userInfo에서 참조하는것은 배열의 주소입니다.
배열 안의 값을 아무리 바꿔도 참조하고 있는것은 배열안의 값이 아니라 객체의 주소이기 때문입니다! push를 하게 되면 기존 배열에 값을 추가하지만 주소값이 변경되는 것은 아닙니다. 그러면 리액트가 봤을 대 새로운 값으로 변했다고 생각하지 않기때문에 리렌더링이 일어나지 않습니다!
handleSubmit 함수에서도 확인하셨듯이 userInfo 배열의 상태값을 바꿔줄 때는 새로운 배열을 만들어서 setUserInfo에 전달해주어야 합니다.
const handleSubmit = (e) => { e.preventDefault(); const newInfo = [...userInfo, { name: name, id: id }]; inputName.current.value = ""; inputId.current.value = ""; inputName.current.focus(); setUserInfo(newInfo); // onSubmit 이벤트가 발생될 때마다 상태값 변경됨 };
useMemo와 useEffect의 차이
useMemo와 useEffect가 둘다 설정된 값의 변동에 따라 동작하는것이라 어떤점이 다른지 이해하기 힘들것 같아 정리해두었습니다.
useMemo
useMemo(실행될것, [값])라고 할때
useMemo는 렌더링 전에 실행되어 메모이제이션한 것을 반환해줍니다. 값이 바뀌었는지 확인하고 렌더링전에 저장된 것을 보내주는것이죠.
useEffect
반면 useEffect는 렌더링 후에 일어납니다.
렌더링후에 상태가 업데이트 되었을때 를 감지하여 동작합니다.
차이점
useMemo는 렌더링에 직접적인 영향을 미칠 수 있습니다, 예를들면 함수에서 컴포넌트등 반환하게 했을때 렌더링하기 이전에 저장된값을 반환하여 리렌더링을 하지 않도록도 할 수 있기때문이죠,
반면 useEffect는 렌더링한 이후에 동작되기때문에 리렌더링을 방지하지 못합니다.
즉 useMemo는 리렌더링이나 불필요한 재실행 과정을 막는 방법(최적화)에 사용하기 적합하고,
useEffect는 상태를 이용한 관리에 사용됩니다.
요약
- 렌더링전 : useMemo, 렌더링후 : useEffect
- 상태변화를 감지하여 effcet를 동작하게 하고싶다면 useEffect
- 값을 저장하여 불필요한 동작 또는 렌더링을 막아 최적화 하고싶다면 useMemo