이전 프로젝트를 진행하면서 useEffect를 사용했을 때 오류가 발생했던 부분이 있었다. 이를 해결하기 위해 구글링을 해본 결과 해당 부분에서는 useCallback을 사용해야 한다는 걸 알게 되었다. 그당시 useCallback은 사용해본 적도 없고, 자세하게 알고 있던 부분이 아닌지라 useEffect랑 유사한 친구구나.라는 정도만 알고 넘어갔다. React에 대해서도 이젠 자세하게 공부해볼 필요가 있다고 생각했고, 그렇게 이번에 공부하기로 결정한 것은 useEffect와 useCallback이다. 왜 useMemo까지 추가했냐.. 이 둘의 차이를 알아보기 위해 검색을 했는데 useMemo와 함께 정리를 글들이 정말 많았다. useMemo는 들어보기만 했던 hook이기 때문에 useMemo까지 포함하여 3개의 hook을 한 번 정리하기로 했다.
컴포넌트가 렌더링 될 때마다 특정 작업(Side effect)을 실행할 수 있도록 한다.
Side effect는 component가 렌더링 된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 뜻한다. (ex) 데이터 페칭(Data Fetching), 구독/리스너 설정(Subscriptions/Listeners), 로컬 저장소(Local Storage) 사용)
API를 통해 데이터를 불러오는 경우 useEffect 내부에서 정의함으로써 처음 렌더링이 일어난 이후에 실행되고 그 이후로는 실행이 되지 않는다. 이는 useEffect 내부에 들어간 부분이 아닌 부분에서 변화가 일어났을 때 useEffect 내부의 값은 랜더링이 되지 않게 함으로써 성능이 저하되는 것을 막을 수 있다.
1
useEffect(() => {
// 렌더링 될때마다 실행시킬 코드
},[]);
2
useEffect(() => {
// 처음 렌더링될 때 한 번만 실행시킬 코드
},[]);
3
useEffect(() => {
// name이라는 값이 업데이트 될 때만 실행시킬 코드
},[name]);
const [data, setData] = useState(null);
useEffect(() => {
// 데이터 페칭을 한 번만 해야 하지만, 렌더링될 때마다 실행됨
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => setData(data));
});
const [count, setCount] = useState(0);
// handleClick 함수는 매 렌더링마다 새로 생성됨
const handleClick = () => {
setCount(count + 1);
};
useEffect(() => {
console.log('Effect executed');
// 함수가 의존성 배열에 포함되어 있어 무한 루프 발생
}, [handleClick]);
React가 의존성 배열의 값들이 변경되었는지 확인할 때 얕은 비교(shallow comparison)를 사용하기 때문에 발생한다. 간단히 말해 useEffect의 의존성 배열을 비교할 때, 객체나 함수의 경우 값의 내용을 비교하지 않고, 참조(reference)가 변경되었는지 확인한다. 이는 객체나 함수가 새로 생성될 때마다 참조가 변경된다는 의미이다. 즉, 컴포넌트가 렌더링될 때마다 함수가 새로 생성되기 때문에 무한루프에 빠지게 되는 것이다.
이를 해결하기 위해 useCallback을 사용할 수 있다. 이에 대한 건 아래에서 한 번에 설명하겠다.
const [items, setItems] = useState([1, 2, 3]);
const [count, setCount] = useState(0);
useEffect(() => {
// 상태 업데이트가 트리거되어 무한 루프 발생
setCount(count + 1);
}, [items]);
items 배열이 변경되지 않았음에도 useEffect가 실행되게 된다. 그 이유는 count가 업데이트되면 컴포넌트가 다시 렌더링되기 때문이다. 2번에서 설명했듯이 컴포넌트가 렌더링될 때마다 함수가 새로 생성되기 때문에 무한루프에 빠지게 되는 것이다.
이를 해결하기 위해 useRef를 사용할 수 있다. useRef는 이번 게시글에서는 다루지 않는다.
const [user, setUser] = useState({ name: 'John', age: 30 });
useEffect(() => {
setUser({ name: 'Jane', age: 35 }); // 객체 업데이트
}, [user]); // user 객체가 의존성 배열에 포함
해당 경우에도 위와 같은 이유로 무한루프에 빠지게 된다. 실제로 user를 변경해주지 않고, 다른 상태를 변경해줄 때에도 컴포넌트가 재렌더링되게 되면 무한루프에 빠지게 된다.
이를 해결하기 위해 useMemo를 사용할 수 있다. 이에 대한 건 아래에서 한 번에 설명하겠다.
이를 통해 알 수 있는 건 참조타입은 이름과 값이 같아도 주소가 다르다면 다르다고 판별하기 때문에 dependencies에 정의할 때 타입이 참조 타입이라면 useCallback, useMemo 등으로 해당 타입을 감싸줘야 한다. 그렇다면 useCallback과 useMemo는 어떻게 다른걸까?
메모이제이션(memoization)은 값비싼 함수 호출의 결과를 캐싱하고 동일한 입력이 다시 발생할 때 캐싱된 결과를 반환하는 프로그래밍 기술로 동일한 입력으로 여러 번 호출되는 함수 또는 컴포넌트가 있을 때 유용하다. 메모이제이션를 사용하면 동일한 결과를 불필요하게 다시 계산하지 않고, 캐시된 결과를 반환할 수 있다. useCallback과 useMemo는 memoization hook이다.
원하는 타이밍에 호출시킬 함수를 만들기 위해 사용하는 hook. 메모이제이션된 함수를 반환해준다.
React 컴포넌트 함수 안에 함수가 선언이 되어 있다면 이 함수는 해당 컴포넌트가 랜더링될 때 마다 새로운 함수가 생성된다.
const add = () => x + y;
이때 useCallback을 사용하면, 함수가 x, y가 바뀌면 새로운 함수가 생성되어 add 변수에 할당되고, x와 y가 동일하다면 다음 렌더링 때 이 함수를 재사용한다.
const add = useCallback(() => x + y, [x, y]);
인라인 콜백함수: 원하는 타이밍에 동작시킬 동작
의존성 리스트: 의존성에 포함된 요소가 업데이트 될 경우, 반환되는 함수도 갱신됨
컴포넌트가 렌더링될 때마다 함수를 새로 선언하는 것은 성능 상 큰 문제가 되지 않는다. 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있다.
원하는 값을 업데이트하기 위해 사용하는 hook. 메모이제이션된 값을 반환해준다.
useMemo는 함수를 호출하고 이 함수에서 리턴된 값을 리턴한다. useMemo를 호출할 때마다 dependencies를 먼저 확인한다. 이때 dependencies에서 바뀐 값이 없다면 캐싱된 값을 리턴하고, 바뀐 값이 존재한다면 다시 함수를 호출하고 새롭게 연산된 값을 리턴한다.
const [count, setCount] = useState(0);
const memoizedCount = useMemo(() => count, [count]);
useEffect(() => {
// 여기에 작업 수행
}, [memoizedCount]);
계산 결과가 메모되어 있으므로 계산에 대한 입력이 변경되지 않으면 업데이트되지 않는다. 즉, useMemo는 계산에 대한 입력이 일정하게 유지되는 경우에만 유용하다. 또한 useMemo는 메모된 결과에 액세스할 때마다 계속 연산을 수행해야 하기 때문에 잦은 변동이 있는 경우 오히려 성능이 악화될 수 있다.
useCallback은 의존성이 거의 없는 가벼운 기능에 가장 적합하며, useMemo는 계산 집약적인 작업에 가장 적합하다.
이전까지는 사실 대부분의 React 코드에서 useEffect만을 사용했다. 그러다가 문제가 가끔 나기도 했는데 코드를 제대로 짜지 못했겠지라는 생각만 했지. 해결 방법을 자세하게 알아볼 생각을 못해서 문제가 나지 않을 정도로만 코드를 변경했던 기억이 있다. 그리고 이번 프로젝트에서 useEffect 무한루프의 원인에 대해 자세하게 알아볼 기회가 있었고, 그로인해 useCallback을 사용하여 해결했다. 이를 계기로 이번 공부 주제를 결정하게 됐는데 역시 내가 모르던 React 세계는 너무 넓고, 항상 왜 이럴까.. 싶었던 것들을 공부할수록 해결해나가는 것 같아서 기분이 되게.. 신기하다. 진짜 점점 이해해나가는 느낌이 너무 좋은 것 같다. React를 쓰면서 hook이라는 것에 지레 겁을 먹을 때가 많았는데, 이번 공부를 통해 조금 겁을 안 먹을 수 있을 것 같다. 아직 모르는 hook들이 조금 남긴했지만, 다음에 나머지 hook들에 대해서도 꼭 정리를 해봐야겠다.
React Hooks 총정리해보기(useState, useEffect, useCallback, useMemo, useRef, useReducer)
React - useEffect, useCallback
🔥 React: 리액트 useEffect 무한 루프 탈출하기
useMemo와 useCallback는 왜, 언제 사용할까?
React Hooks 총정리해보기(useState, useEffect, useCallback, useMemo, useRef, useReducer)