React 최적화
React 최적화
1. UseMemo
Memoization
이미 계산 해본 연산 결과를 기억 해 두었다가 동일한 연산을 시키면, 다시 연산하지 않고, 기억해 두었던 데이터를 반환 시키게 하는 방법

useMemo
React 에서 컴포넌트가 rendering 될 때 마다 함수가 실행되면 불필요한 연산이 매번 발생하게 된다.
예를 들어 다음과 같은 코드가 있다고 할때,
export default function App() {
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const getAnalysis = () => {
console.log("분석 시작");
const highCount = data.filter((it) => it >= 7).length;
const lowCount = data.length - highCount;
const highRatio = (highCount / data.length) * 100;
return { highCount, lowCount, highRatio };
};
const { highCount, lowCount, highRatio } = getAnalysis();
return (
//
<div className="App">
<div>전체 데이터 개수 : {data.length}</div>
<div>높은 점수 개수 : {highCount}</div>
<div>높은 점수 개수 : {lowCount}</div>
<div>높은 점수 개수 : {highRatio}</div>
</div>
);
}
getAnalysis 함수는 data 의 값이 별경 될 때 마다 호출 되기 때문에 비효율적이다.
그래서 useMemo 를 사용해서 data 의 길이가 변경될 때만 getAnalysis 함수를 호출 하도록 useMemo 라는 함수를 사용한다.
import { useMemo } from "react";
export default function App() {
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const getAnalysis = useMemo(() => {
console.log("분석 시작");
const highCount = data.filter((it) => it >= 7).length;
const lowCount = data.length - highCount;
const highRatio = (highCount / data.length) * 100;
return { highCount, lowCount, highRatio };
}, [data.length]);
const { highCount, lowCount, highRatio } = getAnalysis;
return (
//
<div className="App">
<div>전체 데이터 개수 : {data.length}</div>
<div>높은 점수 개수 : {highCount}</div>
<div>높은 점수 개수 : {lowCount}</div>
<div>높은 점수 개수 : {highRatio}</div>
</div>
);
}
최적화 하고 싶은 함수를 useMemo 함수로 감싸게 되면, 해당 함수는 두번 째 매개변수로 들어간 dependency array 가 변경이 되었을 때만 호출하게 되고 그 함수의 반환 값을 반환 해준다.
이처럼, useMemo 를 사용하게 되면 re-rendering 이 발생할 경우, 특정 변수가 변할 때에만 useMemo 에 등록한 함수가 실행되도록 처리하면 함수를 값처럼 사용해서 연산 최적화를 할 수 있다.
2. React.memo
비효율적인 Component Rendering

setCount(10);
setText("Hello");
App 컴포넌트 의 count state를 setCount로 변경하게 되면, App 컴포넌트가 랜더링 되고 자식 컴포넌트 들인 CountView 컴포넌트와 TextView 컴포넌트가 rendering된다.
count state가 바뀔 때 TextView 컴포넌트가 쓸데 없이 rendering 되는 낭비가 발생하고
,text state가 바뀔 때, CountView 컴포넌트가 rendering 되는 낭비가 발생한다.
비효율적인 rendering을 막기 위해 count state만 변경됬을 때는 CountView 컴포넌트만 , text state만 변경됬을 때는 TextView 컴포넌트만 rendering되어야 한다.
이러한 문제를 해결하기 위해서는 함수형 컴포넌트 에선 컴포넌트에게 업데이트 조건을 걸어 원하는 컴포넌트만 rendering이 되도록 하는 React.memo 을 사용 한다 !!
Rendering 문제 예시
import React, { useState, useEffect } from "react";
const CountView = ({ count }) => {
useEffect(() => {
console.log(`CountView is update count: ${count}`);
});
return <div>{count}</div>;
};
const TextView = ({ text }) => {
useEffect(() => {
console.log(`TextView is update text: ${text}`);
});
return <div>{text}</div>;
};
const OptimizeText = () => {
const [count, setCount] = useState(1);
const [text, setText] = useState("");
///
return (
<div style={{ padding: 50 }}>
<div>
<h2>count</h2>
<CountView count={count} />
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</div>
<div>
<h2>text</h2>
<TextView text={text} />
<input
value={text}
onChange={(e) => {
setText(e.target.value);
}}
text={text}
/>
</div>
</div>
);
};
export default OptimizeText;
OptimizeText Component 에서 count 또는, text State 가 변경 될 때마다 CountView Component 와 TextView Component 둘 다 rendering 이된다.
React.memo
import React, { useState, useEffect } from "react";
const CountView = React.memo(({ count }) => {
useEffect(() => {
console.log(`CountView is update count: ${count}`);
});
return <div>{count}</div>;
});
const TextView = React.memo(({ text }) => {
useEffect(() => {
console.log(`TextView is update text: ${text}`);
});
return <div>{text}</div>;
});
const OptimizeText = () => {
const [count, setCount] = useState(1);
const [text, setText] = useState("");
///
return (
<div style={{ padding: 50 }}>
<div>
<h2>count</h2>
<CountView count={count} />
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</div>
<div>
<h2>text</h2>
<TextView text={text} />
<input
value={text}
onChange={(e) => {
setText(e.target.value);
}}
text={text}
/>
</div>
</div>
);
};
export default OptimizeText;
Component 를 React.memo 로 감싸주게되면, 해당 Component 는 prop 의 값이 변경 될 때만 Rendering 이 된다.
React.memo 에서 areEqual 사용
prop 의 값이 Promitive Type 일 경우 에는 해당 prop 의 값이 변경이 되지 않으면 rendering 이 되지 않지만, Object 와 같은 Reference Type 일 경우 에는 주소 값을 저장하기 때문에 prop 의 값이 변경 되지 않아도 다른 값으로 인식하기 때문에 rendering 이 된다. 이때, 다음과 같이 두번째 매개변수로 areEqual 함수를 넘겨주어 rendering이 되도록 수정할 수 있다.
import React, { useEffect, useState } from "react";
const CounterA = React.memo(({ count }) => {
useEffect(() => {
console.log(`CountA Update - count : ${count}`);
});
return <div>{count}</div>;
});
const CounterB = ({ obj }) => {
useEffect(() => {
console.log(`CountB Update - count : ${obj.count}`);
});
return <div>{obj.count}</div>;
};
const areEqual = (prevProps, nextProps) => {
if (prevProps.obj.count === nextProps.obj.count) {
return true;
}
return false;
};
const MemoizedCounterB = React.memo(CounterB, areEqual);
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [obj, setObj] = useState({
count: 1
});
return (
<div style={{ padding: 50 }}>
<div>
<h2>Counter A</h2>
<CounterA count={count} />
<button onClick={() => setCount(count)}>A Button</button>
</div>
<div>
<h2>Counter B</h2>
<MemoizedCounterB obj={obj} />
<button onClick={() => setObj({ count: 1 })}>B Button</button>
</div>
</div>
);
};
export default OptimizeTest;
3. useCallback
useCallback
useCallbakc 은 함수를 메모이제이션 하기 위해서 사용하는 React Hooks 으로, useMemo 와 비슷하다.
useMemo 는 함수를 값처럼 사용해서 연산 최적화를 위해서 사용한다면, useCallback 은 특정 함수를 재정의(새로 만들지 않고) 하지 않고 재사용 하기 위해 사용한다.
useCallback(() => {}, []);
useCallback 이 필요한 이유
React.memo 는 전달된 prop 을 비교할 때 얕은 비교를 하기 때문에 prop 이 ‘값’ (Promitive Type) 이 아니라 ‘객체’ 나 ‘함수’ (Reference Type) 일 경우, prop 에 변화가 있는 것으로 판단하여 Component 를 re-rendering 한다. 즉, 이를 최적화 하기 위해서는 prop 에 변화가 있는 것으로 판단하지 않도록 해당 함수를 useCallback 으로 감싸줌으로써 재생성 하지못하도록 하여 이 문제를 해결할 수 있다.
useMemo 를 사용하면 안되는 이유 useMemo 는 함수 반환이 아닌 값을 반환하기 때문에 함수 자체를 반환하기 위해서 useCallback 을 사용한다.
import React, { useCallback, useEffect, useRef, useState } from "react";
import "./styles.css";
const ComponentA = ({ data, deleteData }) => {
useEffect(() => {
console.log("ComponentA is render");
});
return (
<div>
{data.map((it) => (
<div>
<div>{it.id}</div>
<button
onClick={() => {
deleteData(it.id);
}}
>
삭제
</button>
</div>
))}
</div>
);
};
//React.memo 전달된 prop이 변경될 때만 랜더링
const ComponentB = React.memo(({ createData }) => {
useEffect(() => {
console.log("ComponentB is render");
});
return (
<div>
<button onClick={createData}>데이터 삽입</button>
</div>
);
});
export default function App() {
const [data, setData] = useState([]);
const num = useRef(0);
const createData = () => {
num.current++;
setData([...data, { id: num.current }]);
};
const deleteData = (id) => {
setData(data.filter((it) => it.id != id));
};
return (
<div className="App">
<ComponentA data={data} deleteData={deleteData} />
<ComponentB createData={createData} />
</div>
);
}
예를 들어, 위와 같은 상황에서 App Component 의 data state 는 Component B의 button (데이터 삽입) 을 누르게 되면 data state 가 변경되기 때문에 App Component 가 re-rendering 하게 된다.
이 때, App Component 의 createData 함수를 재선언 하게 되는데, createData 함수는 reference type 이므로 Component B 는 prop 인 createData 함수가 변경되었다고 판단하고 Componet B 가 re-rendering 된다.
따라서, useCallback Hook 을 사용 하여 App Component 가 rendering 될 때, createData 함수가 재선언 되지 않도록 하여 이 문제를 해결 할 수 있다.
useCallback 사용
함수가 재선언 되지 않게, 최적하를 하기 위해 함수를 useCallback 으로 감싸준다.
import React, { useCallback, useEffect, useRef, useState } from "react";
import "./styles.css";
const ComponentA = ({ data, deleteData }) => {
useEffect(() => {
console.log("ComponentA is render");
});
return (
<div>
{data.map((it) => (
<div>
<div>{it.id}</div>
<button
onClick={() => {
deleteData(it.id);
}}
>
삭제
</button>
</div>
))}
</div>
);
};
//React.memo 전달된 prop이 변경될 때만 랜더링
const ComponentB = React.memo(({ createData }) => {
useEffect(() => {
console.log("ComponentB is render");
});
return (
<div>
<button onClick={createData}>데이터 삽입</button>
</div>
);
});
export default function App() {
const [data, setData] = useState([]);
const num = useRef(0);
const createData = useCallback(() => {
num.current++;
setData((data) => [...data, { id: num.current }]);
}, []);
// const createData = () => {
// num.current++;
// setData([...data, { id: num.current }]);
// };
const deleteData = (id) => {
setData(data.filter((it) => it.id != id));
};
return (
<div className="App">
<ComponentA data={data} deleteData={deleteData} />
<ComponentB createData={createData} />
</div>
);
}
useCallback 의 defendency array 에 빈 배열로 전달하면, setData 함수의 data 는 App Component 가 Mount 되는 시점의 data 값(빈 배열) 을 전달하기 때문에 최신 state 값을 유지 할 수 없다.
그렇다고 defendency array 에 data 의 값을 넣어주면 data 가 삭제되거나 수정 될 때 마다 App Component 가 re-rendering 이 되기 때문에 딜레마에 빠지게 된다.
이 때, 함수형 업데이트를 사용하여 최신 state 를 참조할 수 있게 하여 이 문제를 해결 할 수 있다.
setData((data) => [newItem, ...data]);
함수형 업데이트 setState 함수에 함수를 전달하는 방식 state 값을 초기화 해줌으로써 defendency array 를 비워도 항상 최신의 state를 참조 할 수 있게 한다.
UseReducer
UseReducer
useReducer 는 useState 처럼 Component 의 상태 관리를 Component 안에서 사용하는 것과 달리, Component 의 상태관리를 Component 밖으로 분리하기 위해 사용하는 React Hook 이다.
Component 의 구조가 복잡해지는 경우 useState 를 대신하여 상태 변화 로직 들을 Component 밖에서 사용함으로써 Component 를 더욱 가볍게 작성 할 수 있도록 도와준다.
useReducer 가 필요한 이유
useState 를 이용하여 state 를 관리 하다보면, 코드가 길어지고 복잡해 질 수 있다.
만약 하나의 Component 의 여러개의 state 가 존재 하거나 Object state 를 가지게 된다면 해당 Component 가 무거워 질 수 있기 때문에 useState 를 사용한 상태관리는 되도록 지양하고 useReducer 를 사용하여 component 상태 변화 로직을 분리하는 것이 필요하다.
useReducer 사용
const Counter = () => {
const [count, setCount] = useState(1); //useState 로 state 관리
const add1 = () => {
setCount(count + 1);
}
const add10 = () => {
setCount(count + 10);
}
const add100 = () => {
setCount(count + 100);
}
const add1000 = () => {
setCount(count + 1000);
}
return (
<div>
{count}
<button onClick={add1}> add 1 </button>
<button onClick={add10}> add 10 </button>
<button onClick={add100}> add 100 </button>
<button onClick={add1000}> add 1000 </button>
</div>
)
}
예를들어, 위처럼 useState 를 사용하여 count state 를 1, 10, 100, 1000 을 더하도록 상태관리를 할 수 있다. 이를 useReducer 를 사용하면 다음과 같다.
const reducer = {state, action} => {
switch(action.type)
case 1:
return state + 1;
case 10:
return state + 10;
case 100:
return state + 100;
case 100:
return state + 1000;
default:
return state;
}
const Counter = () => {
const [count, dispatch] = useReducer(reducer, 1);
return (
<div>
{count}
<button onClick={() => dispatch({ type : 1 })}> add 1 </button>
<button onClick={() => dispatch({ type : 10 })}> add 10 </button>
<button onClick={() => dispatch({ type : 100 })}> add 100 </button>
<button onClick={() => dispatch({ type : 1000 })}> add 1000 </button>
</div>
}
}
-
const [count, dispatch] = useReducer(reducer, 1);
- 비구조화 할당을 통해 state(count) 와 dispatch 함수를 할당 받는다.
- 상태변화를 처리하는 reducer 라는 함수를 useReducer의 첫번째 인자로 전달한다.
- state의 초기값을 두번째 인자로 전달한다.
-
dispatch({type:1})
- 액션 객체를 type 이라는 property 를 이용하여 dispatch 함수로 전달한다.
-
const reducer = (state, action) ⇒ {}
- dispatch 함수로 전달 받은 액션 객체는 reducer 함수로 전달되어 상태 변화를 처리한다.
- 현재 상태인 state 를 첫번째 인자로 전달한다.
- dispatch 함수로 전달 받은 액션 객체를 두번 째 인자로 전달한다.
Context
Context
Context 는 React Component 트리 안에서 전역 데이터를 공유 하고 관리하여 Component 의 단계 마다 일일이 props 를 넘겨주지 않고도 Component 트리 전체에 데이터를 제공할 수 있다.

Context 가 필요한 이유
프로젝트의 Component 트리 구조가 아래와 같을 때, 가장 하위에 있는 DiaryItem Component 에 prop을 전달하기 위해서는 최상단 Component 인 App Component 에서 DiaryList Component 에 prop 을 넘겨주고 이를 다시 DiaryItem Component 에 전달하는 방식을 따르게 된다. 이러한 props 드릴링은 너무 번거롭고 규모가 커질 수록 복잡해 지기 때문에 이를 해결하기 위해서 Context 라는 개념이 생겼다.

Context 사용
-
Context 생성
React 패키지에서 제공하는 createContext 함수를 사용하여 Context 를 생성한다.
그리고 외부 컴포넌트에서 사용할 수 있도록 export 를 사용해서 내보낸다.
import { createContext } from "react"; export const DiaryStateContext = React.createContext();
-
Context 저장
Context.Provider 로 감싸주어 하위에 모든 컴포넌트들이 value 에 저장되어있는 전역데이터에 접근할 수 있게 된다.
return ( <DiaryStateContext.Provider value={data}> <div className="App"> <DiaryEditor /> <DiaryList /> </div> </DiaryStateContext.Provider> );
-
Context 접근
useContext 를 이용하여 Context 에 저장된 전역 데이터에 접근할 수 있다.
const { data } = useContext(DiaryStateContext);
중첩된 Context 사용
하나의 Context 의 value prop 에 변수 뿐만 아니라 함수도 포함하여 전달할 경우 Provider 또한 Component 이기 때문에 prop이 변경 될 경우 Provider 가 재생성 되면서 하위 Component 들도 재생성 되게 된다. 예를들어 아래에서 data 가 변경 될 때 마다 DiaryStateContext.Provider 하위에 있는 모든 Component 들이 re-rendering 되기 때문에 DiaryStateContext 에 상태변경 함수를 넣어주게 되면 함수 또한 재성성이 되므로 최적화가 풀려 버릴 수 있다. 이러한 경우 중첩 Context 를 사용한다.
return (
<DiaryStateContext.Provider value={data, onCreate, onEdit, onRemove}>
<div className="App">
<DiaryEditor />
<DiaryList />
</div>
</DiaryStateContext.Provider>
);
다음과 같이 DiaryStateContext 하위에 별도의 Context 를 생성해서 data 가 변경되어도 DiaryDispatchContext 에 전달한 상태변경 함수는 변동이 없기 때문에 최적화를 유지할 수 있다.
export const DiaryDispatchContext = React.createContext();
const memoizedDispatch = useMemo(() => {
return { onCreate, onRemove, onEdit };
}, []);
return (
<DiaryStateContext.Provider value={data}>
<DiaryDispatchContext.Provider value={memoizedDispatch}>
<div className="App">
<DiaryEditor />
<DiaryList />
</div>
</DiaryDispatchContext.Provider>
</DiaryStateContext.Provider>
);