1. 문제 상황
- useBackgroundImage 훅에서 backgroundImageUrl 값이 업데이트 됨
- Main 컴포넌트에서 backgroundImageUrl 값이 제대로 반영되지 않는 현상
- 비동기 요청을 통해 배경 이미지를 가져오는 useBackgroundImage 훅을 사용하고 있다. 그러나 이 훅에서 상태 업데이트가 발생했음에도 불구하고, Main 컴포넌트에서 backgroundImageUrl 값이 제대로 반영되지 않아 새로고침 시 업데이트된 이미지가 렌더링되지 않는 문제가 발생한다.
2. 코드 설명
- 기존 코드 구조 : useQuery와 useEffect를 활용한 비동기 요청
- backgroundImageUrl 상태 관리 방식과 발생한 문제점
- localStorage와 상태 불일치 문제
# 기존 코드
# Main
export default function Main() {
const { isLoading, error, backgroundImageUrl } = useBackgroundImage();
if (isLoading) return <Loading />;
if (error) return <div>An error occurred: {error.message}</div>;
console.log('main페이지 : ', backgroundImageUrl);
return (
<main
className='mainBackground'
style={{ backgroundImage: `url(${backgroundImageUrl})` }}
>
<Helmet>
<title>Momentum</title>
<link rel='canonical' href='https://react-momentum-one.vercel.app/' />
</Helmet>
<Navbar />
<DarkModeProvider>
<TodosProvider>
<section className='sectionBackground'>
<Outlet />
</section>
</TodosProvider>
</DarkModeProvider>
</main>
);
}
# useBackgroundImage
export function useBackgroundImage() {
const [backgroundImageUrl, setBackgroundImageUrl] = useState(
() => localStorage.getItem('backgroundImageUrl') || ''
);
const url = `https://api.unsplash.com/photos/random?query=dark&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`;
const fetchImage = useCallback(async () => {
try {
const response = await axios.get(url);
const data = response.data;
localStorage.setItem('backgroundImageUrl', data.urls.full);
localStorage.setItem('lastFetchTime', Date.now());
return data.urls.full;
} catch (error) {
throw new Error('Failed to fetch background image');
}
}, [url]);
const { isLoading, error } = useQuery({
queryKey: ['backgroundImage'],
queryFn: fetchImage,
enabled: !backgroundImageUrl,
staleTime: 1000 * 60 * 60 * 5,
onSuccess: (data) => setBackgroundImageUrl(data),
});
console.log('backgroundImageUrl', localStorage.getItem('backgroundImageUrl'));
useEffect(() => {
const lastFetchTime = parseInt(localStorage.getItem('lastFetchTime'), 10);
if (
!backgroundImageUrl ||
Date.now() - lastFetchTime > 1000 * 60 * 60 * 5 ||
!lastFetchTime
) {
fetchImage();
}
}, [backgroundImageUrl, fetchImage]);
return { isLoading, error, backgroundImageUrl };
}
- 현재 코드는 useQuery와 useEffect를 활용하여 비동기적으로 이미지를 가져오고, 이를 backgroundImageUrl 상태로 관리한다. 기존 코드에서는 backgroundImageUrl이 변경될 때마다 useEffect가 실행되도록 설정되어 있지만, 상태 업데이트가 Main 컴포넌트에 반영되지 않는 이슈가 발생하고 있다. 이로 인해 로컬스토리지에서 가져온 값과 상태 값의 불일치가 나타났다.
3. 문제 분석 및 추측
- 비동기 요청과 상태 관리 불일치 원인
- useEffect와 useQuery 간의 상태 업데이트 흐름 분석
- 비동기 로직의 렌더링 타이밍 문제
- 문제의 원인 : 상태 업데이트와 비동기 로직 간의 불일치
1. 상태 업데이트와 비동기 호출의 순서 :
- useBackfgroundImage 훅에서 fetchImage가 호출될 때, 이는 비동기 작업이다. 즉, 이 함수는 데이터를 가져오는 동안 다른 코드의 실행을 차단하지 않는다.
- fetchImage가 호출되면, 새로운 이미지를 가져오고 이를 setBackgroundImageUrl을 통해 상태로 설정한다. 하지만 이 상태 업데이트는 비동기적으로 처리된다. 즉, setBackgroundImageUrl이 호출된 직후에는 Main 컴포넌트가 재렌더링되지 않는다.
2. useEffect의 의존성 배열 :
- useEffect가 backgroundImageUrl을 의존성으로 가지고 있을 때, 이 값이 변경되면 useEffect가 실행된다. 하지만 비동기 호출이 완료되고 상태가 업데이트된 후, Main컴포넌트에서 이 상태를 반영을 하지 못하고 있다. 상태가 업데이트된 시점에 Main 컴포넌트는 이미 이전 상태를 사용하고 있기 때문에, 새로 업데이트된 backgroundImageUrl값을 반영하지 못하는 문제가 발생한다.
3. 렌더링 시점 :
- React에서는 상태가 업데이트되면 컴포넌트가 리렌더링된다. 그러나 상태 업데이트가 비동기 작업의 결과로 발생할 경우, 다음 렌더 사이클에서 새로운 상태를 반영하기 위해서는 추가적인 로직이 필요한가?
< 요약 >
* useBackgroundImage 훅에서 업데이트된 backgroundImageUrl 값이 왜 Main 컴포넌트에서는 업데이트되지 않았는가?
- 비동기 함수와 리렌더링 타이밍의 불일치 : 훅에서 이미지를 불러오는 fetchImage 함수가 비동기적으로 동작하는데, 비동기 함수가 완료된 후 상태가 변경되면 컴포넌트는 다시 렌더링되어야 한다. 그러나 이 비동기 작업이 끝난 시점에 Main 컴포넌트는 이미지 렌더링을 한 상태고, 새로 받은 backgroundImageUrl로 재렌더링하지 않았기 때문에 변경이 반영되지 않는다.
- useEffect에서 의존성 문제 : useEffect가 backgroundImageUrl을 의존성 배열에 넣고 실행되고 있지만, 비동기 함수가 완료된 시점에서 이 값이 즉시 변경되지 않음. backgroundImageUrl 이 업데이트되어도 컴포넌트에 반영되는 타이밍이 맞지 않아서 리렌더링이 지연되거나 아예 발생하지 않음.
4. 해결 방법
- 방법 1 : useQuery에서 refetch 사용
- 방법 2 : useEffect 내부에서 fetchImage 호출 명확히 설정하기
- 해결책의 핵심은 비동기 작업이 완료되었을 때 상태가 정확히 업데이트되고, 그 상태 변경이 컴포넌트에 반영되도록 만드는 것.
방법 1: useQuery에서 refetch 사용해보기
const { isLoading, error, refetch, backgroundImageUrl } = useQuery({
queryKey: ['backgroundImage'],
queryFn: fetchImage,
enabled: !backgroundImageUrl,
staleTime: 1000 * 60 * 60 * 5,
onSuccess: (data) => {
setBackgroundImageUrl(data);
refetch(); // refetch를 명시적으로 호출해 상태를 업데이트
},
});
- useQuery훅을 사용할 때 refetch를 활용하면, 상태가 업데이트되었을 때 강제적으로 데이터를 다시 가져오게 할 수 있다.
방법 2 : useEffect에서 비동기 작업을 명확하게 관리
useEffect 내부에서 비동기 작업을 처리하는 로직을 더 명확하게 만들어서, 상태가 변경되었을 때 컴포넌트가 재렌더링되도록 해보기
useEffect(() => {
const fetchImageData = async () => {
const lastFetchTime = parseInt(localStorage.getItem('lastFetchTime'), 10);
if (!backgroundImageUrl || Date.now() - lastFetchTime > 1000 * 60 * 60 * 5 || !lastFetchTime) {
const newImageUrl = await fetchImage(); // 비동기 호출
setBackgroundImageUrl(newImageUrl); // 상태 업데이트
}
};
fetchImageData(); // 비동기 함수 실행
}, [backgroundImageUrl]); // 의존성에 backgroundImageUrl 추가
5. 최종 코드
- 문제 해결 후 개선된 코드 설명
- 각 수정사항의 역할과 의도
- 이 문제의 핵심 이슈는 useQuery와 useEffect에서 각각 상태 업데이트와 비동기 로직이 충돌하는 방식에 있었다.
1. 기존 코드의 문제
기존 코드는 useQuery를 사용하여 이미지를 받아오는 비동기 로직을 처리하고 있었다. 하지만 이 방식에서 문제가 발생한 이유는 리렌더링과 의존성 관리 때문이었다.
useQuery에서 enabled: !backgroundImageUrl 조건이 backgroundImageUrl이 비어있을 때만 쿼리를 활성화하는데, 이게 한 번 backgroundImageUrl이 설정되면, 다시 이미지를 받아오는 자동 갱신 기능이 비활성화되는 결과를 가져왔다.
또한 useEffect에서 fetchImage()를 호출했지만, useQuery와의 중복 호출 문제가 있었고, 리렌더링이 필요할 때 적절히 일어나지 않는 상황이 발생했다.
즉, 이미지를 가져오는 타이밍과 상태 업데이트 시 리렌더링이 제대로 이루어지지 않았던 것이다.
2. 해결된 코드의 작동 방식
수정된 코드에서 중요한 변화는 비동기 로직을 useEffect로 명확하게 분리하고 상태를 업데이트하는 방식이다.
* 비동기 로직을 명확히 분리
이전에는 useQuery와 useEffect가 각각 비동기로 이미지를 받아오는 로직을 처리하고 있었는데, 이제는 useEffect 내부에서만 이미지를 가져오는 비동기 함수를 처리한다. 이렇게 하면 중복 호출 문제도 사라지고, 상태 업데이트 흐름도 더 명확해진다.
* 리렌더링 처리
이제 useEffect에서 이미지를 받아오고 setBackgroundImageUrl(newImageUrl)로 상태를 업데이트하면, 리액트가 이 상태 변화를 감지하고 자동으로 리렌더링을 트리거한다. 이전에는 상태 변화가 제대로 리렌더링을 트리거하지 않아서 이미지가 Main 컴포넌트로 전달되지 않는 문제가 있었는데, 이제는 이 문제가 해결됐다.
3. 해결된 이유 요약
의존성 관리: useEffect가 backgroundImageUrl을 의존성으로 가지면서 상태 변화에 따라 비동기 함수가 정확하게 실행됨.
리렌더링 보장: setBackgroundImageUrl로 상태를 업데이트하면 리액트가 자동으로 리렌더링하게 되어, 새로운 값이 Main 컴포넌트에도 전달됨.
중복 호출 방지: useEffect에서 비동기 로직을 한 번에 처리하면서, 중복된 API 호출이 사라지고 코드 흐름이 더 명확해짐.
이렇게 해서 이전에는 리렌더링이 되지 않아 생겼던 문제가 해결
"두 개의 서로 다른 비동기 호출이 동시에 상태를 업데이트하려 하다 보니, 어떤 값이 실제로 상태에 설정되는지가 불명확하여 상태값이 업데이트되는 타이밍과 리렌더링 시점이 맞지 않는 희안한 문제가 있었고, 중복 호출이 안 일어나도록 분리하여 해"
6. 정리 및 배운 점
- 비동기 함수와 상태 관리 시 주의할 점
- React의 상태 관리와 렌더링 간의 동기화 중요성
- useEffect와 useQuery를 동시에 사용할 때는 신중해야 함을 배웠다. 두 훅을 함께 사용할 때 고려해야 할 점은 다음과 같다.
1. 중복 호출 방지 : 두 훅 모두 데이터 fetching과 관련된 작업을 할 수 있으므로, 같은 메서드를 서로에서 호출하게 되면 중복으로 호출될 수 있다. 이를 방지하기 위해, 하나의 훅에서 데이터를 가져오고 다른 훅에서는 상태를 관리하는 방식으로 분리하는 것이 좋다.
2. 조건문 활용 : 두 훅이 같은 상태에 접근하게 된다면, 어떤 훅이 상태를 업데이트하냐에 따라 리렌더링이 발생할 수 있다. 상태 변경이 필요한 경우, 어떤 훅이 그 상태를 책임지는지를 명확히 해야 한다.
3. 의존성 배열에 포함되는 변수를 신중하게 선택해야 한다.
- 비동기 메서드든 일반 메서드이든 그냥 중복 호출 및 상태 일관성을 위해 useEffect와 useQuery에서 같은 메서드를 사용하지 말자.
7. 참고 자료
- 비동기 요청과 React 상태 관리 관련 서치 참고
'Project' 카테고리의 다른 글
싱글톤 자체는 메모리 사용량을 줄이지 않는다. (1) | 2024.11.09 |
---|---|
[React Project] Momentum - Lighthouse 최적화 (5) | 2024.11.06 |
[React Project] Momentum ver 1 .0 .1 (0) | 2024.10.08 |
[React Project]F.ROCK🥻(Shopping mall) (0) | 2024.08.15 |
[React Project] 파지직TV⚡[Ver2.0] (0) | 2024.07.27 |