위의 글을 읽고 원문에 예제는 따로 나와 있지 않아 학습할 겸 정리 및 추가 예제를 적으려고 합니다.
다음은 시니어 리액트 개발자의 기술 인터뷰를 진행하는 동안 질문을 받을 수 있는 매우 일반적인 몇 가지 질문들입니다. 🚀🚀🚀
1.1.1) useMemo 사용하기
useMemo란? 생성(create)함수와 의존성 값의 배열의 값을 받아 의존성이 변경되었을 때에만 메모이제이션된 값만 다시 반환한다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
이렇게만 보면 이해하기 어려우니 예제를 보도록 하겠습니다.
enum FOOD_ENUM {
fast_food = 'FAST_FOOD',
korean = 'KOREAN',
japanese = 'JAPANESE',
chinese = 'CHINESE_FOOD'
}
interface FoodType {
id: number,
name: string,
type: FOOD_ENUM;
}
const Index:NextPage = () => {
const [number, setNumber] = useState(1);
const foods = [
{
id: 0,
name: '김밥',
type: FOOD_ENUM.korean,
},
{
id: 1,
name: '김치찌개',
type: FOOD_ENUM.korean,
},
{
id: 2,
name: '된장찌개',
type: FOOD_ENUM.korean,
},
{
id: 3,
name: '돈부리',
type: FOOD_ENUM.japanese,
},
{
id: 4,
name: '초밥',
type: FOOD_ENUM.japanese,
},
{
id: 5,
name: '짜장면',
type: FOOD_ENUM.chinese,
}
];
useEffect(() => {
console.log('foods 재 호출');
},[foods]);
return(
<>
<h2>숫자 증가</h2>
<input type="number" value={number} onChange={(e) => setNumber(Number(e.target.value))} />
<h2>객체 표시</h2>
{
foods && foods.map((food:FoodType) =>
<p key={`food-${food.id}`}>[{food.type}] {food.name}</p>
)
}
</>
);
};
export default Index;
분명 useEffect에 deps로 foods 값만 추가되어 있는데 number 값이 바뀔 때 마다 food도 다시 불러오는 걸 확인해 볼 수 있습니다.
왜 이런 일이 일어날까요?
자바스크립트에서의 객체는 원시 타입과는 다르게 값이 저장될 때 주소 값으로 저장하기 때문입니다. 따라서 리액트에서 number state가 바뀌면 해당 컴포넌트가 재호출되고 foods의 주소값이 변경되었기 때문에 useEffect에서 foods가 변경되었다고 감지해 함수가 호출되는 것입니다.
이걸 해결하기 위해서는 useMemo를 사용해서 food값을 memoized해서 쓸데없는 리랜더링을 방지할 수 있습니다.
enum FOOD_ENUM {
fast_food = 'FAST_FOOD',
korean = 'KOREAN',
japanese = 'JAPANESE',
chinese = 'CHINESE_FOOD'
}
interface FoodType {
id: number,
name: string,
type: FOOD_ENUM;
}
const Index:NextPage = () => {
const [number, setNumber] = useState(1);
const foods = useMemo(() => {
return [
{
id: 0,
name: '김밥',
type: FOOD_ENUM.korean,
},
{
id: 1,
name: '김치찌개',
type: FOOD_ENUM.korean,
},
{
id: 2,
name: '된장찌개',
type: FOOD_ENUM.korean,
},
{
id: 3,
name: '돈부리',
type: FOOD_ENUM.japanese,
},
{
id: 4,
name: '초밥',
type: FOOD_ENUM.japanese,
},
{
id: 5,
name: '짜장면',
type: FOOD_ENUM.chinese,
},
{
id: 6,
name: '탕수육',
type: FOOD_ENUM.chinese,
},
{
id: 7,
name: '피자',
type: FOOD_ENUM.fast_food,
},
{
id: 8,
name: '햄버거',
type: FOOD_ENUM.fast_food,
},
]
}, []);
useEffect(() => {
console.log('foods 재 호출');
},[foods]);
return(
<>
<h2>숫자 증가</h2>
<input type="number" value={number} onChange={(e) => setNumber(Number(e.target.value))} />
<h2>객체 표시</h2>
{
foods && foods.map((food:FoodType) =>
<p key={`food-${food.id}`}>[{food.type}] {food.name}</p>
)
}
</>
);
};
export default Index;
보시다시피 useEffect가 다시 호출되지 않는 걸 확인할 수 있습니다.
좀 더 극단적인 예를 살펴보도록 하겠습니다.
import { NextPage } from 'next';
import { useState } from 'react';
const expensiveCalculation = (num:number) => {
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
const Index: NextPage = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const calculation = expensiveCalculation(count);
const increment = () => {
setCount((c) => c + 1);
};
const addTodo = () => {
setTodos((t) => {
return [...t, '새로운 할일'];
});
};
return (
<div>
<div>
<h2>할 일</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
<button onClick={addTodo}>할 일 더하기</button>
</div>
<div>
숫자: {count}
<button onClick={increment}>+</button>
<h2>계산 결과</h2>
{calculation}
</div>
</div>
);
};
export default Index;
expensiveCalculation(count)가 실행될 때 count를 참조하기 때문에 count값이 바뀌면 expensiveCalculation(count)함수가 재실행되는 건 정상동작이나 addTodo함수가 호출 될 때도 expensiveCalculation(count)가 재호출되고 있습니다. 따라서 할 일 더하기의 동작이 매우 느린 것을 확인해 볼 수 있습니다. 이것도 useMemo를 통해서 개선할 수 있습니다.
import { NextPage } from 'next';
import { useMemo, useState } from 'react';
const expensiveCalculation = (num:number) => {
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
const Index: NextPage = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const calculation = useMemo(() => expensiveCalculation(count), [count]);
const increment = () => {
setCount((c) => c + 1);
};
const addTodo = () => {
setTodos((t) => {
return [...t, '새로운 할일'];
});
};
return (
<div>
<div>
<h2>할 일</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
<button onClick={addTodo}>할 일 더하기</button>
</div>
<div>
숫자: {count}
<button onClick={increment}>+</button>
<h2>계산 결과</h2>
{calculation}
</div>
</div>
);
};
export default Index;
useMemo를 통해 expensiveCalculation 결괏값을 memorization 시켜놓고 count 값이 바뀔 때만 재 호출하도록 해놨기 때문에 할 일 더하기 이벤트가 매우 빠르게 동작하는 것을 확인해볼 수 있습니다.
1.1.2) useCallback 사용하기
const cachedFn = useCallback(fn, dependencies)
useCallback은 useMemo와 다르게 메모리제이션된 함수를 반환합니다.
주로 api를 요청하는 함수에 useCallback을 사용하는 것이 좋습니다.
만약 하위 컴포넡트가 React.memo()로 최적화 되어 있고 함수를 props로 넘길 때 useCallback으로 함수를 선언하는 게 유용합니다.
예제를 보도록 하겠습니다.
import { useCallback, useEffect, useState } from 'react';
interface Props {
getCount: () => number;
}
const ChildComponent = ({ getCount }: Props) => {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(getCount());
console.log('랜더링');
}, [getCount]);
return <p>{count}</p>;
};
const ParentComponent = () => {
const [dartMode, setDartMode] = useState(false);
const returnCount = (): number => {
return 1000;
};
const getCount = useCallback(() => {
return 1000;
}, []);
return (
<>
<p>다크 모드 : {dartMode ? '다크 모드' : '그냥 모드'}</p>
<input type="button" onClick={() => setDartMode(!dartMode)} />
<ChildComponent getCount={returnCount}></ChildComponent>
{/* <ChildComponent getCount={getCount}></ChildComponent> */}
</>
);
};
export default ParentComponent;
그렇다면 useCallback은 언제 사용하면 좋을까요???
1.1.3) React.memo 사용하기
React.memo(Component, [areEqual(prevProps, nextProps)]);
React.memo는 props가 변경되어 있지 않으면 memorization된 컴포넌트를 그대로 반환하게 됩니다.
따라서 같은 props로 렌더링이 자주 일어나는 컴포넌트에 적용하게 되면 리랜더링을 되는 시점에 성능상 이점을 얻게 됩니다.
예제를 살펴보겠습니다.
import { useState } from "react";
import Child from "./Child";
const Index:NextPage = () => {
const [number, setNumber] = useState(0);
return(
<>
<p>{number}</p>
<input type="button" value="Button" onClick={() => setNumber(number + 1)} />
<Child />
</>
);
};
export default Index;
import React, { useState } from "react";
const expensiveCalculation = (num:number) => {
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
const Child = () => {
const [value] = useState(expensiveCalculation(0));
return (
<>
<p>CHILD: {value}</p>
</>
);
};
export default Child;
속도가 느린 걸 확인할 수 있습니다.
자식 컴포넌트를 다음과 같이 변경하면 컴포넌트가 memorization이 되면서 속도가 굉장히 빨라집니다.
import React, { useState } from "react";
const expensiveCalculation = (num:number) => {
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
const Child = () => {
const [value] = useState(expensiveCalculation(0));
return (
<>
<p>CHILD: {value}</p>
</>
);
};
// export default React.memo(Child);
memorization은 말그대로 메모리를 소모하게 됩니다. 따라서 무조건 useMemo, useCallback, React.memo를 남발하는 게 아니라 주의깊게 사용하는 것이 중요합니다.
가끔씩 map을 사용해 배열을 list를 랜더링시킬 때 다음과 같은 경고문구를 본 적이 있을 겁니다.
왜 그런 걸까요???
React는 리랜더링 과정 중 Reconciliation 과정 중에 Virtual Dom을 통해 두 엘리먼트 트리를 만들고 Diff 알고리즘을 통해 그 트리를 비교하고 변경된 부분만 변경해줍니다.
이 때 개발자는 key prop을 통해, key가 지정되어 있으면 key가 같은 node끼리만 비교하므로 여러 랜더링 사이 중에 어떤 자식 element가 변경되지 않아야 할지 표시해 줄 수 있습니다.
출처: https://www.eduzek.com/reacts-virtual-dom-vs-real-dom/
여기서 주의할 점은 인덱스를 key값으로 사용하면 안된다는 것이다. 만약 delete나 update를 통해 list의 순서가 바뀌게 되면 key값이 전부 바뀌기 때문에 key를 사용하는 이유가 없어지기 때문입니다.
참고 :
https://ko.reactjs.org/docs/hooks-reference.html
https://levelup.gitconnected.com/7-interview-questions-every-senior-react-developer-should-know-d85730fb04d5
https://dmitripavlutin.com/react-usecallback/