UI 작업을 하다 보면, 언제나 문제가 되는 것은 이미지를 화면에 반영하는 것이라고 생각한다.
이미지가 잘 나오면 문제가 전혀 없다.
항상 문제는, 이미지가 나오지 않을 때 (즉, 에러) 어떻게 이미지를 UI적으로 나타내야 하는가에 대한 고민을 해봐야 하는 것이다.
위와 같은 이미지가 나온다고 생각해보라. 홈페이지의 완성도가 급격하게 낮아 보일 것이다.
그래서, 어떻게하면 이미지와 관련된 문제를 해결할 수 있을지에 대해 연구를 해본 결과를 미래의 나를 위하여 남겨두려고 한다.
만약, 어떤 UI에서 이미지 태그를 만들고, 이미지를 src에서 불러온다고 생각해보자.
이 때, img 태그는 하나의 엘레멘트이다.
즉, css를 통해 크기가 주어지지 않은 상태라면 src를 다 불러오기 전까진 width나 height이 결정되지 않는다.
때문에, 만약 img의 html 노드가 생성되어도, 해당 노드에 필요한 src값이 존재하지 않고 width,height값이 존재하지 않는다면 src가 로드가 완료되기 전까지 해당 엘레맨트가 없는 영역으로 인해 UI가 뭉개졌다가, src의 이미지를 받은 후 그 크기만큼 영역을 차지하는 깜빡임이 발생한다.
따라서, 항상 img 태그에는 그 영역에 해당하는 크기가 존재하는 것이 좋다. 보통 관습적으로 Container을 하나 두고, 이 컨테이너가 크기를 가지게 하고 image는 width:100%, height:100%를 주어 부모 크기를 따라가게 하는 케이스가 많이 보인다.
늦게 불러오는 케이스는 당연히 사용자의 네트워크 환경이 좋지 않을 때일 것이고, 실패 역시 네트워크 혹은 해당 src가 유효하지 않을 때일 것이다.
네트워크 환경을 늦게 만드는 방법은 크롬에서 매우 간단하다. 개발자 도구를 킨 후 네트워크 탭에 와이파이 형태 아이콘 옆에 화살표를 누르면 속도를 선택할 수 있게 된다.
이렇게 바꿨을 경우, 이미지 로드가 매우 늦어져 UI 적으로 좋지 않다.
이럴 때, 계속 흰 화면이 뜨기 보다 무언가 다른 것으로 default 설정을 해 두었다가 이미지 로드가 완료되면 그것으로 교체되게 만들면 좋을 것이다.
거기에 더해서, 이미지를 불러오는 것 자체를 실패할 때에도 대체 이미지가 있으면 좋을 것이다. (default인 엑박 이미지 아이콘보다는)
참고로, (혹은 궁금해서 찾아볼만한) Image 속성이 무엇인지 타입을 확인해 보면 아래와 같다.
alt :
이미지 랜더링이 네트워크 불량 등으로 인해 실패했을 때 해당 이미지가 무엇이었는지 설명해주는 대체 텍스트
(검색 엔진은 그림 정보를 alt로 얻으며, 스크린리더 등에서 문서를 음성합성으로 표현할 때에도 사용된다.)
crossOrigin :
해당 요청이 CORS 규칙의 적용을 받을 지 아닐 지 설정해주는 attribute
기본적으로 설정하지 않으면 CORS 규칙 적용을 하지 않는다고 선언하는 것과 같다.
""와 "anonymous"는 동일한 의미를 가지며, 특별한 credential 설정 없이 네트워크 요청을 날리겠다는 의미로 해석된다.
"use-credentials"는 네트워크 요청을 하면서 credential과 관련된 데이터를 같이 보내겠다고 선언하는 것과 같다. 즉, CORS 규칙을 적용하겠다고 하는 것이다.
( 이 때에 credential 데이터라 함은 쿠키, SSL credential, authentication header등을 의미한다 )
참고로, 서버가 설령 access-control-allow-origin:* 으로 전부 허용을 했다 하더라도, 클라이언트 측에서 img를 호출하는 부분에 해당 crossOrigin 을 use-credentials로 설정한다면 이미지를 불러오지 않고 브라우저단에서 실패시킨다. origin을 전부로 설정해놓지 말라는 경고문구와 함께 말이다.
decoding :
받아온 이미지를 디코딩하는 방식에 대해서 설정한다.
sync로 설정할 경우, 동기적으로 디코딩을 하게 되어 해당 이미지 디코딩이 페이지 로드와 직접적으로 영향을 주고받는다.
async로 설정할 경우, 비동기적으로 디코딩을 하게 되어 해당 이미지가 페이지 로드에 영향을 주지 않는다. 즉, 이미지가 크면 클 수록 해당 옵션의 메리트가 매우 커진다. 곧 언급 할 "loading:lazy"와 결합되면 좋다.
예를 들어, 이전에 구현을 해 두었던 무한스크롤에서 해당 박스안에 있는 이미지 태그에 "lazy:loading"이 걸려있을 경우, 스크롤을 내리는 순간 네트워크 요청을 하게 된다. 아래의 gif에 있는 네트워크 탭은 요청되는 순간을 보여주고 있다.
origin :
해당 이미지를 요청했던 오리진을 http request에 포함시킬지 아닐지를 결정한다. 해당 내용은 서버에서 통계를 잡기 위해 사용하기 좋다. 예를 들어, origin이라고 설정할 경우 어디에서 요청이 들어왔는 지 서버에서 나타낼 수 있다.
srcset & sizes :
해당 속성은 이미지를 어떤 크기로 요청해서 불러오고, 어떤 사이즈로 랜더링시킬 지 정의한다.
사용 예시는 아래와 같다.
// srcset은 , 기준으로 작은순으로 나열. 앞은 경로이고 뒤는 현재 브라우저의 뷰포트 크기를 뜻한다.
// sizes 역시 , 기준으로 작은순으로 나열. 앞은 미디어 쿼리이고 뒤는 이미지가 랜더링 될 때의 픽셀 크기이다.
<img
srcset="images/heropy_small.png 400w,
images/heropy_medium.png 700w,
images/heropy_large.png 1000w"
sizes="(max-width: 500px) 444px,
(max-width: 800px) 777px,
1222px"
src="images/heropy.png"
alt="HEROPY" />
단, 해당 srcset 옵션은 반응형 웹브라우저같은 곳에서 일부러 사이즈를 변경해보았을 때 잘 작동하진 않았다. 첫 랜더링 당시에 뷰포트를 확인하고 거기에 맞는 이미지를 가져오는 것에는 잘 작동하였다. 연구가 조금 더 필요해 보인다.
조금 말이 새긴 했는데
실질적으로 우리가 사용해야 하는 것은 이미지의 메서드 부분이다.
이미지로드가 실패했을 때 에러 처리를 위한 attributes들을 생성하는 헬퍼 함수에 대해 만들어본 내용은 아래와 같다.
import { ImgHTMLAttributes, ReactEventHandler } from 'react'
export const errorImgLink =
'https://img.freepik.com/free-photo/adorable-kitty-looking-like-it-want-to-hunt_23-2149167099.jpg?w=900&t=st=1674123236~exp=1674123836~hmac=fe05070da05d72107064dd4771b4d8f86c8b0f6aceb96cc8ab9bd69107795051'
const onError: ReactEventHandler<HTMLImageElement> = ({
currentTarget,
}) => {
currentTarget.onerror = null
currentTarget.src = errorImgLink
}
export const genImgAttrs: (
src: string,
addtionals?: ImgHTMLAttributes<HTMLImageElement>,
) => ImgHTMLAttributes<HTMLImageElement> = (src, addtionals) => {
return {
src,
onError,
loading: 'lazy',
...addtionals,
}
}
//사용처 (ex, index.tsx)
//주의점! object spread문법(...)을 설정해둔 attributes들 앞이 아닌 뒤에 작성할 경우,
//덮어쓰기가 되어 작동하지 않으므로 항상 앞에다가 설정한다.
.
.
<ImageBox>
<img {...genImgAttrs(data.image)} alt="carousel_img" />
</ImageBox>
어트리뷰트가 아닌, 이미지에 대한 공용 컴포넌트 형태로 작성하는 방법은 아래와 같다.
function CustomImage(props: ImageProps) {
const [isLoaded, setIsLoaded] = useState(false);
const [isError, setIsError] = useState(false);
const handleImageLoad = () => {
setIsLoaded(true);
};
const handleImageError = () => {
setIsError(true);
};
return (
<Image
{...props}
src={
isError || !props.src
? errorImgLink
: isLoaded
? props.src
: loadingImgLink
}
loading="lazy"
onLoad={handleImageLoad}
onError={handleImageError}
/>
);
}
const Image = styled.img`
width: 100%;
height: 100%;
`;
이것은 블로그 작성을 위해서 여러모로 테스트를 하다 보니 알게 된 내용인데,
next.js 에서는 위 코드(일반 html img element를 사용하는 코드) 에서 에러와 로딩에 관련된 콜백 핸들러("onLoad", "onError") 가 작동하기 위해서는 반드시 <loading="lazy"> 속성이 들어가줘야 하는 것을 확인하였다.
최적화를 위해서 제한되었던 모양인 것인지, 이것은 확인이 조금 더 필요할 것 같다.
이런 저런 이유로 인해, next.js에서는 공식 문서에도 자체적으로 구현되어 있는 "Image"컴포넌트를 사용하도록 권장하고 있다.