img 태그는 src에 있는 url에서 보여주고자 하는 이미지를 가져온다.
필자는 이미지와 이미지의 설명을 동시에 보여주고 싶었다.
그런데, img 태그는 존재하는데 src가 아직 불려와지지 않아서
이미지에 대한 설명과 이미지가 따로 load되는 경우가 있다.
UX를 해치는 부분이라고 생각이 들어 이미지 load에 맞춰 설명을 같이 보여주려고 했다.
어떻게 하면 src까지 완전히 load된 것을 파악할 수 있을까?
문제 상황을 간단하게 영상으로 살펴보자.
이미지가 나오기 전에 텍스트가 먼저 나와버린다.
이미지와 텍스트 데이터는 한번의 API 통신으로 다 받아왔기 때문에
텍스트는 바로 렌더링 되고, 이미지는 src의 url에 들어가 접근하기 때문에 조금 더 시간이 걸리는 모양이다.
// CocktailContainer.tsx
// ... //
function CocktailContainer(props: ImgContainerInterface) {
// ... //
return (
<div>
{props.upward && isImgLoaded && <h2>{props.name}</h2>}
<div className={styles.image_div}>
<img
alt={props.alt}
src={props.src}
className={styles.image}
ref={imgRef}
/>
</div>
// ... //
</div>
);
}
export default CocktailContainer;
부모 컴포넌트에서 API 통신을 한 결과를 props로 받아오고, 이를 보여주는 방식이다.
h2
컴포넌트는 바로 렌더링 되는 반면, img
태그는 불러오는데 시간이 걸린다.
큰 문제는 없어 보이지만...거슬린다!
img
가 완전히 불려와졌는지를 판단하는 아주 좋은 방법이 있다.
바로 complete
라는 속성을 활용하는 것이다.
The read-only HTMLImageElement interface's
complete
attribute is a Boolean value which indicates whether or not the image has completely loaded.
- MDN docs -
MDN 공식문서에서는 complete
속성은 boolean
타입의 값으로 읽기만 가능하다고 한다.
또한, 이미지가 완전히 불려와졌는지 아닌지를 알려주는 속성이라고 한다.
사용법은 대략 아래와 같다.
const img = document.querySeletor("img");
if(img.complete){
console.log("이미지 로딩 완료");
} else {
console.log("이미지 로딩 중");
}
좀 더 확실하게 하고 싶다면, img
의 naturalHeight
를 사용하는 것도 방법이다.
If the intrinsic height is not available—either because the image does not specify an intrinsic height or because the image data is not available in order to obtain this information, naturalHeight returns 0.
- MDN docs -
만약, img
가 naturalHeight
속성 값을 얻을 수 없다면 0
을 반환한다.
이를 활용해서 img
가 불려왔는지, 렌더링 되고 있는지 판단할 수 있을 것이다.
이제 React 내에서 어떻게 활용할지 살펴보자.
img
태그의 정보를 가져올 필요가 있다. useRef
를 사용하면 될 것 같다.img
태그에 이벤트 리스너를 달아준다. load
이벤트를 바라보면 될 것 같다.complete
와 naturalHeight
를 사용해 이미지 load를 판단한다.// CocktailContainer.tsx
// ... //
function CocktailContainer(props: ImgContainerInterface) {
// img 태그 정보를 가져오기 위한 useRef
const imgRef = useRef<HTMLImageElement>(null);
// 완전히 load 된 상태인지를 담고 있는 useState
const [isImgLoaded, setIsImgLoaded] = useState(false);
// ref를 바라보며, 변경이 생길 때마다 load status를 업데이트 하기위한 useEffect
useEffect(() => {
if (!imgRef.current) {
return;
}
// complete와 naturalHeight를 이용해 완전한 load를 판단하는 함수
const updateStatus = (img: HTMLImageElement) => {
const isLoaded = img.complete && img.naturalHeight !== 0;
setIsImgLoaded(isLoaded);
};
// load 이벤트를 바라본다.
// 익명 함수를 사용했기 때문에 once 속성을 사용해서 한번 실행 후 제거한다.
imgRef.current.addEventListener(
"load",
() => updateStatus(imgRef.current as HTMLImageElement),
{ once: true }
);
}, [imgRef]);
return (
<div>
{props.upward && isImgLoaded && <h2>{props.name}</h2>}
<div className={styles.image_div}>
<img
alt={props.alt}
src={props.src}
className={styles.image}
ref={imgRef}
/>
</div>
// ... //
</div>
);
}
export default Information;
결과를 살펴보자.
이제 이미지가 완전히 load 된 후에 텍스트가 보인다!
이 기능은 다른 img
태그에도 적용할 것 같다.
그래서 로직을 분리해서 따로 관리하고 싶었다.
// CocktailContainer.tsx
// ... //
function CocktailContainer(props: ImgContainerInterface) {
const imgRef = useRef<HTMLImageElement>(null);
// custom hook에 ref 전달
// 완전히 load 되기 전에는 false, 된 후에는 true를 반환
const isImgLoaded = useImgLoadStatus(imgRef);
return (
<div>
{props.upward && isImgLoaded && <h2>{props.name}</h2>}
<div className={styles.image_div}>
<img
alt={props.alt}
src={props.src}
className={styles.image}
ref={imgRef}
/>
</div>
// ... //
</div>
);
}
export default CocktailContainer;
// useImgLoadStatus.ts
import { useState, useEffect, RefObject } from "react";
/**
* 이미지의 load 상태를 파악하는 커스텀 hook
* @param ref load 상태를 파악하고자 하는 대상
* @returns 이미지 load 상태
*/
export function useImgLoadStatus(ref: RefObject<HTMLElement>) {
const [isImgLoaded, setIsImgLoaded] = useState(false);
useEffect(() => {
if (!ref.current) {
return;
}
const updateStatus = (img: HTMLImageElement) => {
const isLoaded = img.complete && img.naturalHeight !== 0;
setIsImgLoaded(isLoaded);
};
ref.current.addEventListener(
"load",
() => updateStatus(ref.current as HTMLImageElement),
{ once: true }
);
}, [ref]);
return isImgLoaded;
}
이제 필요한 이 기능을 적용하고 싶은 img
태그에 이 hook을 사용하면 된다.
isImgLoaded
가 load에 따라 업데이트 될 것이고, 이 값을 받아 렌더링에 사용한다.
useEffect
내에서 인자로 받아온 ref
의 load
이벤트를 감지하고,
img
의 완전한 load 여부를 state로 관리하는 것이 목표이다.
그러나...!
잘 된다.
???????
정확히는 처음 렌더링 될 때는 잘 된다.
그런데, 이미지를 변경하는 과정에서 문제가 생겼다.
img
의 src
가 변경되면, ref
를 dependency
로 가지고 있는 useEffect
가 실행될거라고 생각했는데, 그게 아니었다.
useEffect
는 첫 렌더링에서만 실행되었고, img
태그에 변화가 있더라도 다시 실행되는 일은 없었다.
이는 dependency
를 ref.current
로 변경해도 마찬가지였다.
We didn’t choose useRef in this example because an object ref doesn’t notify us about changes to the current ref value.
- React 공식 문서 -
DOM node에 접근하여 값을 얻는 예제를 설명하는 공식 문서에서 발췌한 내용이다.
대략..useRef
를 통해 얻은 ref
는 ref.current
의 변화를 우리에게 알려주지 않는다고 한다.
!!!!
변화를 알려주지 않았기 때문에 useEffect
의 dependency
로 넣어놔도 감지를 못하는 거였다!
일단 useRef
를 useEffect
의 dependency
로 활용할 수 없다는 사실을 알게 되었다.
문제는, 필자가 이미 custom hook을 저 hooks를 사용했다는 것이다...
어떻게 해야 img
태그의 변경을 감지하도록 만들 수 있을까?
방법은 생각보다 간단했다.
1. src
를 dependency
로 사용하는 것
2. useEffect
가 실행될 때 isImgLoaded
를 false
로 초기화하는 것
// CocktailContainer.tsx
const isImgLoaded = useImgLoadStatus(imgRef, props.src);
// useImgLoadStatus.ts
import { useState, useEffect, RefObject } from "react";
/**
* 이미지의 load 상태를 파악하는 커스텀 hook
* @param ref load 상태를 파악하고자 하는 대상
* @returns 이미지 load 상태
*/
export function useImgLoadStatus(
ref: RefObject<HTMLImageElement>,
src: string
): boolean {
const [isImgLoaded, setIsImgLoaded] = useState(false);
// ref가 아닌 src를 dependency로 가진다.
useEffect(() => {
if (!ref.current) {
return;
}
// 한번 load된 상태이기 때문에 isImgLoaded가 true로 되어 있는데,
// 그 것을 초기화하여, 새로운 이미지로 변경되고 있음을 보여준다.
if (isImgLoaded) {
setIsImgLoaded(false);
}
const updateStatus = (img: HTMLImageElement) => {
const isLoaded = img.complete && img.naturalHeight !== 0;
setIsImgLoaded(isLoaded);
};
ref.current.addEventListener(
"load",
() => updateStatus(ref.current as HTMLImageElement),
{ once: true }
);
}, [src]);
return isImgLoaded;
}
이제 img
의 src
가 변경될 때마다 useEffect
가 실행되며,
그 때마다 로딩 상태를 초기화해서 false
에서 true
로 변경되게 한다.
React 특성 상, 새로고침이나 페이지 이동이 없으면 useState
가 초기화되지 않기 때문이다.
필자는 하나의 페이지 내에서 특정 값이 변경될 때 실행되는 custom hook을 사용 중일 뿐이다.
일반적인 함수였다면 호출될 때마다 내부의 모든 값이 초기화되겠지만,
custom hook으로 만들어서 사용 중이므로 이런 차이가 발생하는 것으로 생각된다.
결과는 다음과 같다. UI만 다소 변경되었다.
이제 이미지의 변경이 매번 감지되고 있고,
이름을 보여주는 컴포넌트는 이미지 로딩 상태에 따라 보여졌다가, 감춰졌다가 한다.
만약 여기서 더 깔끔한 UI를 만들고자한다면,
src
가 변경될 때, 아예 이미지를 지웠다가 보여주는 방법을 사용해볼 수도 있겠다.
techiedelight 게시글
Alejandro Martinez님 게시글
MDN docs - img 태그 complete
MDN docs - img 태그 naturalHeight