# React 최적화 3 - useCallback
컴포넌트를 최적화하기 위해서 어떤 컴포넌트가 최적화 대상인지 찾아낼 수 있어야 한다
어떻게 찾아야 할까?
React Developer Tools의 기능을 활용하자!
Highlight updates when components render 기능을 체크해주면 화면에서 변경이 일어나는 컴포넌트에 테두리 색이 보이면서 어떤 컴포넌트가 렌더링 되고 있는 것인지 확인할 수 있다
일기 리스트를 삭제하면 DiaryEditor 컴포넌트도 렌더링 되고 있다
불필요한 렌더링이 발생하고 있기에 DiaryEditor 컴포넌트의 최적화가 필요하다!
# DiaryEditor 컴포넌트 최적화
지난 시간에 배운 React.memo를 사용해서 최적화를 해보자
컴포넌트 코드가 길 때는 전체 컴포넌트를 묶지 말고 가장 하단의
export default React.memo(DiaryEditor);
에 React.memo 로 묶어주자
언제 렌더링이 일어나는지 useEffect를 사용해서 확인해보자
useEffect(() => {
console.log("렌더링이 일어났다");
});
콘솔창을 확인해보니 DiaryEditor 컴포넌트가 두번 렌더링이 일어나고 있다
App.js 컴포넌트를 통해 그 이유를 알아보자
function App() {
//일기 데이터 배열 관리
const [data, setData] = useState([]);
//id 값 할당 변수
const dataId = useRef(0);
const getData = async () => {
const res = await fetch(
"https://jsonplaceholder.typicode.com/comments"
).then((res) => res.json());
const initData = res.slice(0, 20).map((it) => {
return {
author: it.email,
content: it.body,
emotion: Math.floor(Math.random() * 5) + 1,
create_date: new Date().getTime(),
id: dataId.current++,
};
});
setData(initData);
};
useEffect(() => {
getData();
}, []);
처음에 data 는 빈 배열로 렌더링된다
getData 함수를 통해서 api를 불러오면 data의 배열이 바뀌며 다시 렌더링된다
DiaryEditor 컴포넌트는 App.js 컴포넌트의 자식 컴포넌트이다
따라서 App.js 컴포넌트의 상태가 변하면 props로 받고 있는 onCreate 도 계속해서 렌더링이 일어난다
onCreate의 재생성을 막아야 한다!
어떻게 막을까?
useMemo는 사용하면 안된다 -> 값을 반환하기 때문
따라서 useCallback 기능을 사용하자
# useCallback 이란?
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
메모이제이션된 콜백 함수를 반환한다
두번째 인자로 전달한 값이 변하지 않으면 첫번째 인자의 콜백함수를 계속해서 재사용할 수 있다
# useCallback을 통한 컴포넌트 최적화
onCreate 함수에 useCallback을 적용해보자
const onCreate = useCallback((author, content, emotion) => {
const created_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
}, []);
DiaryEditor 렌더링이 어떻게 달라지는지 확인해보기 위해서 다이어리 리스트를 삭제해보자
처음에만 렌더링이 일어나고 일기가 삭제되어도 렌더링이 일어나지 않는 것을 확인할 수 있다
새로운 일기를 등록하자 기존의 일기들이 사라지고 새로운 일기 하나만 남았다
기존의 일기들이 왜 사라졌을까?
useCallback을 사용하면서 [] 에 아무런 값도 넣어주지 않았기 때문이다
따라서 현재의 state 값을 사용하지 못하고 있다
const onCreate = useCallback(
(author, content, emotion) => {
const created_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
},
[data]
);
data 값을 반영하도록 두번째 배열에 넣어주어야 하는데 이렇게 하면 다른 작업이 발생했을 때 전과 동일하게 렌더링이 발생할 것이다
dependency array 에 data를 넣어주면 기존 최적화가 무의미해진다
딜레마!!!
함수형 업데이트 활용하자
setData((data)=>[newItem, ...data]);
setState 함수에 함수를 전달하는 것을 함수형 업데이트라고 한다
최신의 state를 인자를 통해 참고하게 되면서 dependency array 값이 없어도 된다
const onCreate = useCallback(
(author, content, emotion) => {
const created_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData((data)=>[newItem, ...data]);
},
[]
);
코드를 수정한 후 다시 한번 일기를 저장해보자
원하는대로 잘 동작하는 것을 확인할 수 있다
# 최적화 완성
다이어리 리스트 삭제 시 모든 각각의 일기들 전부 렌더링되고 있다
따라서 DiaryListItem 컴포넌트 최적화 필요하다
# DiaryListItem 컴포넌트 최적화
React.memo 로 컴포넌트를 묶자
export default React.memo(DiaryListItem);
전과 동일하게 useEffect를 사용해서 어떤 아이템이 렌더링 되고 있는지 확인한다
useEffect(() => {
console.log(`${id}번째 렌더링!!!`);
});
모든 아이템들 렌더링되고 있다
App.js 컴포넌트에서 onEdit, onRemove 를 최적화 해야한다
useCallback 을 사용하여 로직을 최적화하자
const onRemove = useCallback((targetId) => {
setData((data) => data.filter((it) => it.id !== targetId));
}, []);
const onEdit = useCallback((targetId, newContent) => {
setData((data) =>
data.map((it) =>
it.id === targetId ? { ...it, content: newContent } : it
)
);
}, []);
이렇게 적용해주니 일기 삭제, 추가 시에도 최적화가 정상적으로 적용된 것을 확인할 수 있다
# useReducer
상태 변화 로직을 컴포넌트에서 분리해야 한다
왜 분리해야 할까?
컴포넌트의 코드가 길어지고 복잡해지는 것을 해결할 수 있기 때문이다
useReducer는 useState 를 대신해서 사용할 수 있다
# useReducer 적용
App.js 컴포넌트에 useReducer 를 적용시켜보자
기존 useState 코드는 제거하고 useReducer 로 바꾸자
//reducer
const [data, dispatch] = useReducer(reducer, []);
reducer 컴포넌트 기본 형태는 다음과 같다
const reducer = (state, action) => {
switch(action.type) {
return
}
}
App.js 에서 조작하는 데이터 형식을 보고 reducer 컴포넌트의 switch 문을 적절하게 작성하고 기존의 setData 함수를 dispatch 로 변경하자
getData 함수 먼저 고쳐보자
const getData = async () => {
const res = await fetch(
"https://jsonplaceholder.typicode.com/comments"
).then((res) => res.json());
const initData = res.slice(0, 20).map((it) => {
return {
author: it.email,
content: it.body,
emotion: Math.floor(Math.random() * 5) + 1,
create_date: new Date().getTime(),
id: dataId.current++,
};
});
dispatch({ type: "INIT", data: initData });
};
setData 함수 대신 dispatch 를 사용하여 코드를 변경하였다
reducer 에는 INIT 시 발생시킬 로직을 작성하자
const reducer = (state, action) => {
switch (action.type) {
case "INIT": {
return action.data;
}
...
}
};
action 객체에서 data property 를 꺼내서 return 해주면 새로운 state가 된다
이러한 방식으로 나머지 코드도 고쳐준다
const reducer = (state, action) => {
switch (action.type) {
case "INIT": {
return action.data;
}
case "CREATE": {
const create_date = new Date().getTime();
const newItem = {
...action.data,
create_date,
};
return [newItem, ...state];
}
case "REMOVE": {
return state.filter((it) => it.id !== action.targetId);
}
case "EDIT": {
return state.map((it) =>
it.id === action.targetId ? { ...it, content: action.newContent } : it
);
}
default:
return state;
}
};
수정된 함수 코드는 다음과 같다
const onCreate = useCallback((author, content, emotion) => {
dispatch({
type: "CREATE",
data: { author, content, emotion, id: dataId.current },
});
dataId.current += 1;
}, []);
// 일기 삭제 메소드
const onRemove = useCallback((targetId) => {
dispatch({ type: "REMOVE", targetId });
}, []);
// 일기 수정 메소드
const onEdit = useCallback((targetId, newContent) => {
dispatch({ type: "EDIT", targetId, newContent });
}, []);
저장, 수정, 삭제 전부 정상적으로 동작한다
dispatch는 함수형 업데이트 필요 없고 호출하면 reducer 가 현재 state를 참고하기 때문에 편리하다
# Context
DiaryList 에는 두개의 사용하지 않는 props 존재한다
전달만 하는 props가 많이 생기면 수정이 어려운 등의 악영향이 생긴다
또한 부모 컴포넌트에서 자식 컴포넌트로 계속해서 props를 전달하는 것을 props drilling 이라고 하는데 이는 코드를 복잡하게 만들고 불필요한 연산을 증가시킨다
따라서 props drilling 을 해결해야 한다
# props drilling 해결 방법
Provider 컴포넌트를 중간에 배치하여 Provider 컴포넌트에서 하위 컴포넌트들에게 모든 데이터 전달시키자
Provider 컴포넌트가 자식 컴포넌트에게 직통으로 데이터 공급하면 쓸데없는 props 전달 사라진다
이렇게 Provider 컴포넌트와 자식 컴포넌트 간의 data 전달을
Context 문맥
이라고 한다
같은 문맥 내에서 data를 공급하기 때문에 props drilling 해결할 수 있다
# Context API 적용
Context API를 적용해보자
기본 형태는 아래와 같다
//context 생성
const MyContext = React.createContext(defaultValue);
//Context Provider를 통한 데이터 공급
<MyContext.Provider value={전역으로 전달하고자 하는 값}>
{/* context 안에 위치할 자식 컴포넌트들 */}
</MyContext.Provider>
기존의 App.js 컴포넌트 내에 DiaryStateContext 컴포넌트를 생성한다
export const DiaryStateContext = React.createContext();
App.js 의 return 부분에 최상위 컴포넌트로 Provider 컴포넌트를 넣어준다
return (
<DiaryStateContext.Provider value={data}>
<div className="App">
<OptimizeTest />
{/* <Lifecycle /> */}
<DiaryEditor onCreate={onCreate} />
<div>전체 일기 : {data.length}</div>
<div>기분 좋은 일기 개수 : {goodCount}</div>
<div>기분 나쁜 일기 개수 : {badCount}</div>
<div>기분 좋은 일기 비율 : {goodRatio}</div>
<DiaryList diaryList={data} onRemove={onRemove} onEdit={onEdit} />
</div>
</DiaryStateContext.Provider>
);
value props로 data 가 잘 전달되고 있는 것 확인할 수 있다
그렇다면 자식 컴포넌트에서 어떻게 사용할까?
우선 DiaryList 컴포넌트부터 살펴보자
기존에 props로 전달받던 diaryList 를 useContext 를 사용해서 Context에서 가져오자
const DiaryList = ({ onRemove, onEdit }) => {
const diaryList = useContext(DiaryStateContext);
... 기존코드
최적화 상태는 유지하되 onRemove, onEdit 과 같은 함수들을 props로 전달하지 않고 context를 이용하기 위해 새로운 context 하나를 추가로 생성해야 한다
export const DiaryDispatchContext = React.createContext();
... 기존 코드
return (
<DiaryStateContext.Provider value={data}>
<DiaryDispatchContext.Provider>
<div className="App">
<OptimizeTest />
{/* <Lifecycle /> */}
<DiaryEditor onCreate={onCreate} />
<div>전체 일기 : {data.length}</div>
<div>기분 좋은 일기 개수 : {goodCount}</div>
<div>기분 나쁜 일기 개수 : {badCount}</div>
<div>기분 좋은 일기 비율 : {goodRatio}</div>
<DiaryList onRemove={onRemove} onEdit={onEdit} />
</div>
</DiaryDispatchContext.Provider>
</DiaryStateContext.Provider>
);
return 문에 컴포넌트를 추가해준다
이렇게 되면 전체 구조는 다음과 같다
onCreate, onRemove, onEdit 컴포넌트를 하나로 묶어서 DiaryDispatchContext에 전달하자
//useMemo 사용 불필요한 연산 방지
const memoizedDispatches = useMemo(() => {
return { onCreate, onRemove, onEdit };
}, []);
각 컴포넌트들에서 props로 내려받던 state와 함수들을 지우고 useContext로 받아오면 된다
이때 비구조화 할당을 잊지 말아야 한다!
# 새로 알게 된 점
지난 시간부터 리액트의 최적화에 대해서 배웠다
이전에 진행했던 프로젝트는 개발에만 급급해서 최적화를 해봐야지 생각만 하고 실행에 옮기지 못했다
(최적화하는 방법에 대해서 잘 모르기도 했다)
이번 강의에서 배운 useReducer, useContext 는 코드의 가독성을 좋게 하고 개발하는 과정에서도 굉장한 편리함을 주는 기능인 것 같다
복잡하게 props 로 내려주던 데이터들을 하나에서 관리할 수 있다는 것에 놀랐고 실제 구현 코드도 굉장히 간단해서 더 놀라웠다
이번에 배운 최적화 방법을 복습하여 이전에 진행한 리액트 프로젝트의 최적화를 꼭 진행해보아야겠다
프로젝트 리팩토링을 하고 블로그에 과정을 남겨보겠다(최대한 빠른 시일 내에)
'TIL > 프로그래머스 데브코스' 카테고리의 다른 글
클라우딩 어플리케이션 엔지니어링 TIL Day 33 - 감정 일기장 만들기 (2) (0) | 2024.02.23 |
---|---|
클라우딩 어플리케이션 엔지니어링 TIL Day 32 - 감정 일기장 만들기 (1) (0) | 2024.02.23 |
클라우딩 어플리케이션 엔지니어링 TIL Day 30 - React 기본 간단한 일기장 만들기 (3) (1) | 2024.02.17 |
클라우딩 어플리케이션 엔지니어링 TIL Day 29 - React 기본 간단한 일기장 프로젝트 (2) (1) | 2024.02.11 |
클라우딩 어플리케이션 엔지니어링 TIL Day 28 - React 기본 간단한 일기장 만들기 (1) (0) | 2024.02.09 |