tech.junha.kr

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태그가 보여지는 과정은 대략 이렇다.

  1. alt값과 이미지 로드 실패 아이콘이 뜬다. (API 호출을 통해 url을 받아오기 전)

  2. alt값과 이미지 로드 실패 아이콘이 없이 빈 화면이 뜬다. (이미지 소스에 url이 생기기 때문)

  3. 버벅거리면서 불러와진다. (외부 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%;
    }
}