챌린지반에서 지원해 진행하게된 발표 자료다. 다른 아티클도 있었지만 이 내용을 선택한 이유는 지난번에 시간에 쫓겨 제대로 알아보지 못한 useEffect를 제대로 알아보고싶다는 생각이 들어서였다.
단순히 아티클 내용을 설명하는 것이 아니라, 공부하면서 막혔던 부분을 어떻게 해소했는지 / 그리고 이전에 진행한 메달 트래커 과제에서 사용한 useEffect들을 다시 살펴보고 수정하는 내용을 발표했다.
목차
1. 아티클 내용 중 이해가 어려웠던 부분과 해결 방법
2. 메달 트래커 과제에서 사용한 useEffect 돌아보기
useEffect
- 컴포넌트가 렌더링된 후 실행할 동작을 설정할 수 있는 Hook
- 의존성 배열을 이용해 특정 값이 변경됐을 때만 동작을 실행하도록 설정할 수 있음!
Effect가 필요없는 일반적인 경우
- 렌더링을 위해 데이터를 변환하는 경우
- 변경된 state를 정렬해 보여주고 싶은 경우, effect가 다시 state를 변경한다면 정렬을 위해 렌더링 2회
- (불필요한 렌더링 피하기 위해) 컴포넌트의 최상위 레벨에서 계산 가능하다면 Effect에서 처리하지 않기!
- 사용자 이벤트를 처리하는 경우
- 이벤트 핸들러를 사용하는 것이 더 의도를 명확히 할 수 있음
- 이벤트 핸들러는 바로 계산되지만, Effect는 렌더링 후 계산
어려웠던 부분
Effect 없이 일부 state 조정하기
- items를 props로 전달받아 선택된 아이템을 selection에서 관리
- items가 바뀔 때마다 selection을 null로 재설정
피해야할 방법
const List = ({items}) => {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
};
개선안 1
const List = ({items}) => {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
const [prevItems, setPrevItems] = useState(items);
if(items !== prevItems) {
setPrevItems(itmes);
setSelection(null);
}
};
🤯🤯🤯 state 변경이 일어나는 건 마찬가지인데 무슨 차이가 있다는 걸까??????
렌더링 과정에 대한 이해가 충분하지 않아 두 경우의 차이를 느끼기 어려웠다.직접 테스트 코드를 작성해 콘솔에 찍으며 렌더링 과정을 살펴보기로 했다.
function App() {
const [items, setItems] = useState(0);
return (
<>
{ /* 생략 */}
<List items={items} />
</>
);
}
function List({ items }) {
console.log("List 렌더링");
const [selection, setSelection] = useState(0);
// useEffect 사용하는 경우
useEffect(() => {
console.log("useEffect 실행");
setSelection(selection + 1);
}, [items]);
// 개선안 사용하는 경우
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
console.log("prevItem과 비교");
setPrevItems(items);
setSelection(selection + 1);
}
return (
<>
<div>selection {selection}</div>
<ChildComponent selection={selection} />
</>
);
}
function ChildComponent({ selection }) {
console.log("ChildComponent 렌더링");
return <div>ChildComponent {selection}</div>;
}
- 리렌더링 될 모든 요소들이 렌더링 된 후 useEffect가 실행
- List를 렌더링하는 과정에서 발생한 변경사항을 반영 후 나머지 요소 리렌더링
일반적으로 개선안 1은 잘 사용하는 패턴이 아니다!
개선안 2
const List = ({items}) => {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
const selection = items.find(item => item.id === selectedId) ?? null;
};
- items가 변경돼 리렌더링할 때 items에 기존에 선택된 id가 없으면 null로 변경
- 컴포넌트 리렌더링 중에 계산할 수 있으므로 가장 권장되는 방법
외부 저장소 구독하기
일반적인 방법
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}
updateState();
window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
개선된 방법 - useSyncExternalStore 사용
hook에대한 이해가 없어 예제 코드를 이해하기 어려웠다. hook도 모르고, 예시로 사용한 API도 모르니 더 이해가 어렵다고 생각해 내가 아는 (window에서 사이즈 받아오기) 외부 정보를 구독하는 예시로 예제를 만들고 설명을 덧붙여달라고 챗지피티를 이용했다. 해당내용
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
return useSyncExternalStore(
subscribe, // 동일한 함수를 전달하는 한 React는 다시 구독하지 않습니다.
() => navigator.onLine, // 클라이언트에서 값을 얻는 방법
() => true // 서버에서 값을 얻는 방법
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
- 매개변수
- subscribe: 저장소를 구독하는 동작
- 저장소에서 값을 받아오는 동작
- 서버에서 초기값을 제공하는 동작 (optional)
- 구독한 외부 저장소에 변화가 생겼을 때를 감지해 리액트에서 그 값을 반영해 리렌더링
- 사용하는 외부 저장소의 데이터가 바뀌는 것을 반영하기 위해 사용할 수 있음!
useEffect 수정하기
sortOption이 변경될 때마다 데이터 정렬하기
const [medalData, setMedalData] = useState([]);
const [sortOption, setSortOption] = useState("금은동 우선순위");
useEffect(() => {
let sortedData = sortData(medalData);
setMedalData(sortedData);
}, [sortOption]);
기존 방식의 문제점
- 렌더링을 위해 데이터를 변환하는 경우에 해당
- sortOption이 바뀌면서 1번, medalData가 변경되면서 1번 불필요한 리렌더링이 발생
변경
function RankingTable({ medalData, sortOption, deleteHandler }) {
const sortedData = sortData(medalData, sortOption);
return (
<div id="rankingTable">
{/* 생략 */}
<tbody>
{sortedData.map((data) => {
return (
<MedalTableRow
key={data.country}
data={data}
deleteHandler={deleteHandler}
></MedalTableRow>
);
})}
</tbody>
</table>
</div>
);
}
- 데이터를 보여줄 RankingTable 컴포넌트에서 리렌더링 과정 중 정렬을 처리하는 것으로 변경
useMemo를 사용하지 않은 이유
- 아티클에서는 반복되는 연산을 방지하기위해 useMemo를 사용할수도 있다, 1ms 이상의 계산은 이를 활용하라고 소개!
- 궁금해서 소요 시간을 찍어보았으나, 데이터가 추가되어도 큰 차이가 없어 사용하지 않음
첫 로드일 때 localStorage에서 데이터 불러오기
const [initialLoad, setInitialLoad] = useState(true);
const [medalData, setMedalData] = useState([]);
useEffect(() => {
if (initialLoad && localStorage.getItem("medalData")) {
let savedData = JSON.parse(localStorage.getItem("medalData"));
setMedalData(savedData);
setInitialLoad(false);
}
}, []);
기존 방식의 문제점
- 그냥 state의 초기값을 지정해주면 되는데, 굳이 전체 렌더링 → effect로 값 받아와 다시 렌더링
- 지금 구조에서는 한번 사용되고 말 initialLoad를 state로 관리
변경
const [medalData, setMedalData] = useState(() => {
const savedData = localStorage.getItem("medalData");
return savedData ? JSON.parse(savedData) : [];
});
- 초기값을 지정할 때 localStorage에서 값을 받아와 존재한다면 JSON.parse, 없다면 빈 배열을 반환하도록 변경
useSyncExternalStore를 사용하지 않은 이유
- state외의 공간에서 데이터를 관리하는 것은 맞지만, 프로젝트 내 동작을 제외하고 저장소의 데이터를 건드리지 않음
- 따라서 외부 저장소의 변화를 계속 반영해줘야하는 상황이 아님
medalData가 변경될 때마다 localStorage에 저장
const [medalData, setMedalData] = useState([]);
useEffect(() => {
if (initialLoad) return;
localStorage.setItem("medalData", JSON.stringify(medalData));
}, [medalData]);
useEffect를 유지한 이유
- 불필요한 렌더링을 발생시키지 않음
- 어디서 medalData가 변경되더라도 로컬스토리지에 반영할 수 있음
변경
useEffect(() => {
const storageData = localStorage.getItem("medalData");
if (JSON.stringify(medalData) === storageData) return;
localStorage.setItem("medalData", JSON.stringify(medalData));
}, [medalData]);
- initialLoad state를 없애며 medalData 초기값을 설정한 값이 다시 저장되는 걸 방지하는 조건문이 사라짐
- 기존 저장된 데이터와 현재 medalData가 일치하는 경우 진행하지 않도록 수정함
useEffect를 사용하기 전에 생각해보자!
- 단순히 state 변경 후 실행할 동작을 위해 사용하진 않는가?
- props, state로 계산할 수 있다면 렌더링 중에 계산할 수 있는 방법은 없을까?
- 이 동작이 사용자와의 상호작용을 반영하기 위한 것인가?
'챌린지반' 카테고리의 다른 글
[React] useReducer, context API로 투두리스트 만들기 (0) | 2024.08.26 |
---|---|
[React] useState, useEffect 동작 만들어보기 (0) | 2024.08.21 |
[React] CRA, Vite 없이 개발환경 구축하기 (0) | 2024.08.19 |
[React] 리액트 훅, 리액트를 함수로 분석하기 (0) | 2024.08.14 |