React Hooks
리액트에서 제공하는 여러 메서드들은 원래 클래스형 컴포넌트에서만 사용이 가능하다.
그러나 클래스형 컴포넌트는 중복 코드, 문법의 복잡성, 가독성의 문제 등 여러 단점이 있어 리액트 팀에서도 공식적으로 비추천하고 있으며, 메모앱에서 사용했듯 함수형 컴포넌트를 사용하는 것이 좋다.
// 클래스형 컴포넌트
class MemoEditor extends Component { ... }
// 함수형 컴포넌트
const MemoEditor = () => { ... }
하지만, 메모앱에서 사용했던 상태를 관리하는 State 또한 클래스형 컴포넌트에서만 사용할 수 있는 메서드이나, 'useState'를 사용하여 상태를 관리해왔다.
이는, 'use' 키워드를 사용하여 클래스형 컴포넌트에서 사용 가능한 메서드를 낚아채 사용하는 React Hooks이다
이 Hooks를 사용하여 메모앱을 최적화해보자.
1. useMemo
- 메모이제이션(memoization) : 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술
useMemo는 메모이제이션을 하는 hook의 한 종류로, 계산 비용이 많이 드는 함수의 결과 값을 메모이제이션하여 불필요한 재계산을 방지하고 컴포넌트 렌더링 성능을 향상시킬 수 있으며 다음과 같은 형식이다.
const memoizedValue = useMemo(() => {
// 계산이 비용이 많이 드는 작업을 수행하는 함수
return result; // 계산 결과 값
}, [dependencyList]);
첫 번째 인자로 메모이제이션하고자 하는 함수를 전달하고 두번째 인자로 해당 함수가 의존하는 값을 담은 배열인 의존성 배열(dependencyList)을 전달한다. 그러면 배열에 포함된 값들이 변경될 때만 함수가 실행되고 그렇지 않은 경우에는 계산한 결과만 재사용한다.
메모의 주제별 개수를 집계하는 함수를 만들어보자.
App.js
const getMemoAnalysis = () => {
console.log("메모 분석 시작")
const others = data.filter((it) => it.subject === "기타").length;
const shopping = data.filter((it) => it.subject === "쇼핑").length;
const toDo = data.filter((it) => it.subject === "할 일").length;
const study = data.filter((it) => it.subject === "공부").length;
return {others, shopping, toDo, study}
}
// getMemoAnalysis는 지역함수로 작성하였으니 호출.
// 객체로 return했으니 객체로 비구조화 할당을 받음
const {others, shopping, toDo, study} = getMemoAnalysis();
이렇게 되면 App.js에 작성한 함수이기 때문에 화면이 로딩될 때와 data에 변화가 생길 때 해당 로직이 실행된다.
그렇게 되면 메모를 수정할 때도 해당 로직이 실행되는데 이는 불필요한 것이다. useMemo를 사용하여 불필요한 계산을 없애보자.
const getMemoAnalysis = useMemo(
// 첫 번째 인자: 수행될 계산
() => {
console.log("메모 분석 시작")
const others = data.filter((it) => it.subject === "기타").length;
const shopping = data.filter((it) => it.subject === "쇼핑").length;
const toDo = data.filter((it) => it.subject === "할 일").length;
const study = data.filter((it) => it.subject === "공부").length;
return {others, shopping, toDo, study}
// 두 번째 인자(배열): data의 길이가 변할 때만(추가/삭제) 계산 수행
}, [data.length]);
const {others, shopping, toDo, study} = getMemoAnalysis();
이렇게 하면 아래와 같은 오류가 발생한다.
여기서 useMemo는 getMemoAnalysis가 리턴하는 값을 다시 리턴하는 것으로, useMemo Hook으로 선언한 메모이제이션된 값은 콜백함수(getMemoAnalysis)를 호출하여 얻어진 결과값 자체가 되기 때문에 함수가 아니라 변수처럼 사용해야 한다.
// (X)
const {others, shopping, toDo, study} = getMemoAnalysis();
// (O)
const {others, shopping, toDo, study} = getMemoAnalysis;
💡 최적화할 대상을 찾는 법
1. chrome 웹 스토어에서 React Developer Tools를 설치한다.
2. 개발자 도구에서 Components 탭에서 확인한다.
React Developer Tools를 사용하면 사용자의 사용에 따라 어떤 컴포넌트가 작동하는지 알 수 있는데, 메모를 수정하거나 삭제할 때 MemoEditor 컴포넌트도 같이 작동하는 것을 볼 수 있는데 이는 불필요한 동작이다.
컴포넌트는 본인의 State가 변화, 부모 컴포넌트가 리렌더링 혹은 받은 prop이 변경되었을 경우 리렌더링된다.
현재 App 컴포넌트에서 MemoEditor 컴포넌트로 onCreate라는 함수를 prop으로 전달하고 있으며, 이 함수를 통해 data를 추가하고 있다.
그렇기 때문에 data를 수정, 삭제하는 함수가 실행되면 App.js가 리렌더링되고 그 영향으로 상관없는 것 같은 MemoEditor도 같이 리렌더링되는 것이다.
먼저 앞서 사용한 React memo를 사용하여 DiaryEditor를 묶어준다. 이때 DiaryEditor 전체를 memo로 감싸줄 것이기 때문에
export default React.memo(MemoEditor);
이런 식으로 작성하는 것이 좋다. 하지만 위에서 말했든 DiaryEditor가 전달받는 onCreate 는 App 컴포넌트가 렌더링될 때마다 다시 만들어지기 때문에 memo를 사용한다 하더라도 렌더링이 계속 발생하기 때문에 결국은 onCreate가 재생성되지 않아야만 memo와 함께 최적화를 할 수 있다.
하지만 위처럼 onCreate에 useMemo를 사용하는 것은 불가능하다. useMemo는 앞서 말했듯 '값'만을 리턴하는 것이기 때문이다.
React의 또다른 hook인 useCallback을 사용해보자.
2. UseCallback
메모이제이션된 콜백을 반환하는 것으로, 두 번째 인자로 설정한 의존성배열의 값이 변하지 않으면 첫 번째 인자로 설정한 콜백함수를 계속 재사용할 수 있는 것이다. 이는 불필요한 렌더링을 방지하기 위해 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백을 전달할때 유용하다.
const memoizedCallback = useCallback(
() => {
// 첫 번째 인자: 수행할 콜백 함수
doSomething(a, b);
},
// 두 번째 인자: 의존성 배열
[a, b],
);
App.js
const onCreate = useCallback(
(subject, title, content) => {
const writtenDate = new Date().getTime();
const newItem = {
subject,
title,
content,
writtenDate,
id: dataId.current,
}
dataId.current += 1;
setData([newItem, ...data]);
// 빈 배열: App.js가 마운트됐을때만 실행하고 그 뒤로는 실행하지 않음.
},[]);
하지만 이렇게 하면 메모가 추가될 때마다 기존의 메모는 삭제되는 문제가 발생한다.
이는 App.js가 마운트되었을 때 한 번만 실행하도록 하였는데 그 당시의 data의 값이 [](빈 배열)이기 때문이다.
그렇기 때문에 메모를 아무리 추가하여도 다시 빈 배열로 data가 리셋되어보린다.
해결하기 위해 data가 변경될 때 다시 해당 함수가 실행되도록 하려고 의존성 배열에 [data]를 넣으면 이는 data가 변경될 때마다 onCreate를 재생성하지 않도록 하려는 현재 목표와는 맞지 않게 된다.
이는 함수형 업데이트를 사용한다면 해결할 수 있다. 이는 이전 상태를 이용하여 새로운 상태를 간단하게 업데이트 할 수 있는 것인데,
const onCreate = useCallback(
// 중략
setData((data)=>[newItem, ...data]);
},[]);
이는 setData에 값이 아닌 함수를 전달하는 것으로, 새로운 메모의 data를 인자로 받아와 배열에 추가하는 형태이기 때문에 의존성 배열이 빈 배열이라도 최신 상태를 기준으로 업데이트를 수행하게 된다.
마지막 최적화 또한 위와 로직이 비슷하다.
React developer tools를 사용하여 확인해보면, 메모 Item 하나를 삭제할 때마다 모든 item들이 렌더링되는 것을 볼 수 있다.
이는 데이터가 많은 경우에 속도저하가 심각하게 올 수 있기 때문에 꼭 최적화를 해주어야 한다.
위와 마찬가지로 먼저 MemoItem 컴포넌트 전체를 React.memo로 묶어준다.
MemoItem.js
const MemoItem = ({
onEdit, onDelete,
id, subject, title, content,
writtenDate
}) => { ... }
여기서 content를 빼고는 모두 변화하지 않는 데이터이며,
onEdit, onDelete는 위의 onCreate와 마찬가지로 App.js의 data 상태가 변하면 리렌더링되는 것이기 때문에 memo로만 최적화가 불가하고,
App.js의 해당 함수에서 useCallback을 사용해야한다.
App.js
const onDelete = useCallback(
(targetId) => {
// const newMemoBoard = data.filter((it)=>it.id !== targetId);
// setData(newMemoBoard);
// ↓
setData((data)=> data.filter((it)=>it.id !== targetId));
},[]);
const onEdit = useCallback(
(targetId, newContent) => {
// setData(
// data.map((it) => it.id === targetId ? {...it, content:newContent} : it)
// );
// ↓
setData( (data) =>
data.map((it) => it.id === targetId ? {...it, content:newContent} : it)
);
},[]);
onCreate에서와 마찬가지로 data를 인자로 받아오고 데이터를 다루는 식을 함수형 업데이트에 사용해준다.
최적화 완성!
리액트 라이프사이클은 컴포넌트가 실행, 업데이트, 종료되는 과정을 다루는 과정이자 메서드의 집합으로, 컴포넌트는 이러한 메서드를
통해 특정 시점에 코드를 실행하거나 상태를 관리할 수 있다.
- 실행 Mount ex) 초기화 작업
- 업데이트 Update ex) 리렌더, 예외처리
- 종료 unMount ex) 화면에서 사라짐, 메모리 정리
기본적으로 Life Cycle마다 실행할 수 있는 메서드가 존재하는데 (ComponentDidMount, ComponentDidUpdate, ComponentWillUnmount...) 이 메서드들은 클래스형 컴포넌트에서만 사용이 가능하다.
'JS > React Project' 카테고리의 다른 글
🏃♀️ 목표 다이어리 - 글 작성 / 수정 (0) | 2023.08.07 |
---|---|
🏃♀️ 목표 다이어리 만들기 - 홈화면 (0) | 2023.08.01 |
🏃♀️ 목표 다이어리 만들기 - 기획 및 기초 세팅 (0) | 2023.07.28 |
📝 메모앱 업그레이드 (0) | 2023.07.25 |
📝 간단한 메모앱 만들기 (0) | 2023.07.20 |