React 깜빡임 현상 해결: useLayoutEffect와 이미지 로딩
분명 다 만들었지만, 보기엔 흉한 깜빡임 현상!!
이 글에선 두 가지 깜박힘 현상에 대해 다룰 것이다.
1. useEffect 호출에 의한 깜빡임
2. API 호출에 의한 깜빡임
1. UseEffect 호출에 의한 깜빡임

const [duckUrl, setDuckUrl] = useState('');
useEffect(() => {
setDuckUrl('/duck.png');
}, []);
<img alt='duck' src={duckUrl} />
Hook Flow에 의하면 DOM 배치 → 페인트 작업 → useEffect 호출 순서를 따른다.
따라서 useEffect로 이후 페인트 작업이 한 번 더 이루어짐에 따라(리페인트) 깜빡이는 현상이 나타난다.

2. UseLayoutEffect 을 통한 해결

const [duckUrl, setDuckUrl] = useState('');
useLayoutEffect(() => {
setDuckUrl('/duck.png');
}, []);
<img alt='duck' src={duckUrl} />
**DOM 배치 → **useLayoutEffect **→**페인트 작업
첫 페인트 작업이 발생하기 전에 useLayoutEffect 함수가 호출되기 때문에, 깜박임 없이 DOM을 업데이트 할 수 있다.
3. API 호출에 의한 깜빡임
이젠 API 호출에 의한 깜빡인 현상을 알아보자.
API 호출을 통해 이미지 URL을 받아오고, 그 URL을 이미지 소스에 넣으면 보통 다음과 같이 나타난다.
보기만 해도 아찔해지는 버벅거림이다.

오리가 나오기까지 img태그가 보여지는 과정은 대략 이렇다.
-
alt값과 이미지 로드 실패 아이콘이 뜬다. (API 호출을 통해 url을 받아오기 전)
-
alt값과 이미지 로드 실패 아이콘이 없이 빈 화면이 뜬다. (이미지 소스에 url이 생기기 때문)
-
버벅거리면서 불러와진다. (외부 url로 가져오기 때문)
const [duckUrl, setDuckUrl] = useState('');
useEffect(() => {
const getDuck = async () => {
const {
data: { url },
} = await axios.get('https://random-d.uk/api/random');
setDuckUrl(url);
};
getDuck();
}, []);
<img alt="duck" src={duckUrl} />
4. 해결과정(1) url이 준비되면 img태그 랜더링

1번 과정 " alt값과 이미지 로드 실패 아이콘이 뜬다." 을 수정했다.
API 호출이 끝난 지점을 기점으로 조건부 랜더링을 시켜줬다.
하지만 역시 만족스럽지 않다.
커튼처럼 부자연스럽게 내려오는 사진을 보고있자니 마음이 아려온다...
const [duckUrl, setDuckUrl] = useState('');
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getDuck = async () => {
const {
data: { url },
} = await axios.get('https://random-d.uk/api/random');
setDuckUrl(url);
setLoaded(true);
};
getDuck();
}, []);
{ loaded &&
<img alt='duck' src={duckUrl} />
}
5. 해결과정(2) 사진이 다 로드되면, 그때 랜더링 하기

img 태그의 onLoad 이벤트를 통해, 이미지 로드가 될 때 조건부 랜더링을 시켜줬다.
추가로 loaded 되지 않았다면 display:none을 통해 버벅거리는 과정을 보여지지 않게 설정했다.
어느정도 해결이 다 됐지만, 욕심이 생긴다.
이미지가 로딩중이라는 것을 표현하고 싶다.
const [duckUrl, setDuckUrl] = useState('');
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getDuck = async () => {
const {
data: { url },
} = await axios.get('https://random-d.uk/api/random');
setDuckUrl(url);
};
getDuck();
}, []);
<img alt='duck'
src={duckUrl}
onLoad={() => setLoaded(true)}
style={loaded ? { display: "block" } : { display: "none" }}
/>
6. 해결 : skeleton image loading 효과 주기

성공! 기능만 추가했을 때 보다 훨씬 UX가 좋아졌다.
이미지 태그의 부모 태그에 Background-color에 스켈레톤 css 효과를 주어 구현했다.
추후에 관련 로직을 따로 추출하고, 스켈레톤 로딩을 사용하는 다른 컴포넌트에 이식하기 수월하게 리팩토링을 해줘야겠다.
import axios from 'axios';
import { useLayoutEffect, useEffect, useState } from 'react';
import './skeleton.css';
function App() {
const [loaded, setLoaded] = useState(false);
const [duckUrl, setDuckUrl] = useState('');
useLayoutEffect(() => {
const getDuck = async () => {
const {
data: { url },
} = await axios.get('https://random-d.uk/api/random');
setDuckUrl(url);
};
getDuck();
}, []);
return (
<div style={{ backgroundColor: 'black' }}>
<div style={{ width: 500, height: 500 }} className='skeleton'>
<img
alt='duck'
src={duckUrl}
style={loaded ? { display: 'block' } : { display: 'none' }}
onLoad={() => setLoaded(true)}
/>
</div>
</div>
);
}
export default App;
:root {
--loading-grey: #ededed;
}
img {
display: block;
width: 500px;
height: 500px;
}
.skeleton {
background-color: #ededed;
background: linear-gradient(
100deg,
rgba(255, 255, 255, 0) 40%,
rgba(255, 255, 255, 0.5) 50%,
rgba(255, 255, 255, 0) 60%
);
background-size: 200% 100%;
background-position-x: 180%;
animation: 1s loading ease-in-out infinite;
}
@keyframes loading {
to {
background-position-x: -20%;
}
}