next/image는 어떻게 생겼나요?

okkkkkky·2024년 6월 17일
0

이전 글에서, next.js 기반의 프로젝트를 lighthouse를 통해 검사할 때 next/image를 권장하는 문구들이 많은 것을 알 수 있었습니다. 단순히 next/image를 적용하는 것을 넘어서 이 패키지는 어떻게 구성되어있으며 어떤 방식으로 최적화를 할 수 있는지 리서치한 글입니다.

next.js에서 이미지 최적화 하기

next.js에서 제공하는 next/image에서는 html img태그와는 달리, 아래의 기능들을 가능하게 합니다.

  • 사이즈 최적화: viewport에 따라 이미지 사이즈를 최적화합니다.
  • 시각적 안정성: layout shift를 방지합니다.
  • 빠른 페이지 로딩: lazy loading, placeholder를 제공하여 페이지 로딩을 더욱 빠르게 합니다.

위의 기능들은 next/image에서 어떻게 가능하게 하는지 직접 next/image 내부 소스코드를 통해 확인해보고자 합니다.

next/image

아래는 import Image from "next/image"를 통해 사용되는 Image 태그에 대한 파일의 내용입니다.

handleLoading

이미지를 로드할 때 다양한 로딩 상태에 대한 처리 핸들러함수입니다. (중간에 생략된 부분들이 있음)
handleLoading

  • 전달받은 img prop을 기반으로 디코딩을 하거나 그대로 사용합니다.
  • 여기에서 onLoad함수를 ref로 전달하며, load event 함수를 새로 생성하는 로직이 추가됩니다. 여기에서 current target으로 전달받은 image를 설정해주는 로직이 추가됩니다.
  • onLoadingCompleteRef에서는 props로 전달받은 이미지를 current값에 설정해줍니다.
  • 이외에 unoptimized 설정 혹은 data-XXX props로 받은 값들로 이미지 사이즈의 비율을 설정하거나 경고 문구를 출력합니다.

getDynamicProps

fetchPriority 값을 설정에 React 버전에 따라 반환하는 값을 다르게 해주는 중간 파싱함수입니다.
fetchPriority

ImageElement

html의 img태그를 next.js만의 img로 변형하는 컴포넌트입니다.
ImageElement

  • forwardRef를 사용하여 handleLoading 함수를 호출합니다. 내부에 useCallback으로 감싸주어서, 이미지와 관련된 함수 생성의 최적화를 진행시켜줍니다. img의 onLoad만으로는 image의 로딩 상태에 대해서 대응해줄 수 없으며, 이미지 로드 완료 후의 작업이나, 에러가 발생했을 때의 작업 등을 ref를 통해 유연하게 다룰 수 있습니다. 이 작업들을 위해서는 이미지의 상태에 대해 계속해서 모니터링이 필요합니다.
  • ref 말고, img 자체 속성인 onLoad에서도 마찬가지로 handleLoading 함수를 호출합니다.

ImagePreload

이미지 리소스를 미리 로드할때 사용되는 컴포넌트입니다. 이를 통해 페이지 로드 속도를 향상시킬 수 있고, 동시에 웹 바이탈의 주요 지표 수치인 lighthouse의 lcp 점수를 향상시킬 수 있습니다.
ImagePreload

  • ReactDom의 preload를 통해 (react 18버전 이상의 경우) 위에서 설정한 옵션들의 값과 이미지 src를 통해 preload를 하고 null을 return하며 종료합니다.
  • 이외의 케이스들에 대해서 link 태그를 통해서 preload를 할 수 있도록 합니다.

Image

위의 컴포넌트 및 함수들을 조합하여 아래와 같이 실제 next/image에서 import하여 사용하는 Image가 완성됩니다.
Image-next/image

  • appRouter인지 pageRouter인지 먼저 구분을 해줍니다. (next.js의 버전 구분)
  • useEffect를 통해 ImageElement 및 handleLoading에서 사용될 onLoadRef.current값에 props로 넘겨받은 onLoad 함수를 설정해줍니다. (onLoadingComplete도 마찬가지)
  • props와 local state를 포함하여 ImageElement를 렌더링하고, priority가 설정되는 경우, ImagePreload 컴포넌트도 같이 렌더링 해줍니다.

그렇다면, priorty를 설정하게 되었을때 link 태그 혹은 reactDom의 preload를 통해 미리 로드되는 것은 알았다면, viewport에 따른 이미지 사이즈 최적화 혹은 placeholder는 어떻게 구성되어있을까요? 이 내용들은 Image 컴포넌트 내부에서 사용되는 getImgProps라는 함수가 포함되어있는 파일에서 확인해볼 수 있습니다.

getImgProps

이 함수에서는 placeholder의 설정값에 따라 blur된 이미지의 스타일링을 구성하거나 viewport에 따른 이미지 비율 조절 등 전반적인 이미지 스타일 설정을 해주는 함수입니다.

getWidths

next.config.js에서는 이미지의 device 사이즈를 정의할 수 있는 기본 옵션이 있습니다. 이 기본옵션을 사용하거나 Image 컴포넌트에서 명시해준 sizes string이나 를 기반으로 하는 스타일을 적용할지 정의할 수 있습니다.

getWidths

  • sizes 속성이 있는 경우에는, vw 단어를 찾아서 percentSizes 배열을 구성합니다. 이를 기반으로 widths와 kind:"w"인 값을 반환합니다.
  • sizes 속성이 없고, Image 컴포넌트에 width가 숫자로 기입되지 않은 경우, config 파일에 선언한 device 사이즈를 widths 범위로 정의합니다.
  • width가 숫자로 되어있는 경우에는, 최적의 이미지 너비를 정의하여 반환합니다 (new Set 로직)

placeholder

placeholder가 되는 이미지는 기본값이 "empty"로 설정되어있는 placeholder 속성에 다른 값이 있는 경우에 대해 내부적인 background image를 만들고 이를 스타일 객체로 만듭니다. 이 placeholder는 앞서 봤던 handleLoading 함수에서 이미지 load가 완료된 다음, setBlurComplete(true)로 변경되면서, 원래 이미지가 나타나고, 기존의 blur 이미지는 사라지게 되는 원리로 로딩 중에는 blur이미지가 나오게 됩니다.

(참고) setBlurComplete는 Image 컴포넌트 내부에서 useState로 선언된 local state의 setState 함수입니다.

placeholder

next/server에서의 이미지 최적화

위와 같이 Image태그를 사용하여 next.js 내부에서 이미지 자체의 최적화는 되었지만 이후 next server에서의 캐싱과정은 어떻게 진행되는 것인지 같이 알아보겠습니다.

handleNextImageRequest

우선 next image를 호출하는 함수입니다.

handleNextImageRequest

  • _next/image로 요청되는 경우가 아니라면 false를 반환합니다.
  • imageOptimizerCache를 통해 새로운 이미지 최적화 캐시를 생성 및 초기화 합니다.
  • paramsResult는 요청파라미터로부터 특정함수를 통해 얻은 결과물입니다. 이를 통해 cache에서 cacheKey를 얻습니다 (cacheKey)
  • this.imageOptimizer를 통해 buffer, contentType, maxAge를 계산하고,
  • 이 결과들을 기반으로 cache 항목들을 반환하여 cacheEntry를 구성합니다.
  • 이 값들로, response를 보내주며 함수는 종료됩니다.

imageOptimizer

꼬리에 꼬리를 물며, 위의 handleNextImageRequest 함수 내부에 있는 require(./image-optimizer)에서는 어떤 작업을 해주는걸까요? next/server에 있는 image-optimizer.ts 파일에는 이미지를 최적화하여 캐싱하는 작업을 확인할 수 있습니다.
이 파일에서는 중요한 클래스 및 함수 3가지 정도만 확인해보도록 하겠습니다.

ImageOptimizerCache

handleNextImageRequest 함수에서 이미지 최적화 캐시를 생성하고 초기화 해줬던 역할을 하는 클래스입니다. 이 클래스를 통해 캐시를 관리할 수 있습니다.

getCacheKey

ImageOptimizerCache 객체 안에 있는 static 메소드로 말그대로 이미지의 href, width, quality, mimeType을 기준으로 hash function을 실행시켜 얻은 반환값, 즉 cache key를 얻는 함수입니다.
getcachekey 함수

getter 함수

마찬가지로 해당 객체 안에 있는 메소드입니다. cacheKey를 기반으로 maxAge, expireAt 등의 정보 및 buffer 파일을 가져올 수 있고, 이를 기반으로 이미지 데이터 및 캐시에 대한 정보들을 전달해줍니다.
getter 함수

setter 함수

객체 안에 있는 함수로, cache에 값을 저장하는 비동기 함수입니다.
setter 함수

  • 저장하려는 값이 image가 아니거나 revalidate의 type이 숫자가 아닌 경우, 에러를 반환합니다.
  • writeToCacheDir를 통해 캐시값을 저장합니다.

optimizeImage

함수의 말그대로 이미지를 최적화시키는 함수입니다. next.js에서도 언급되었던 것처럼 내부에서 sharp 라이브러리를 사용하여 최적화 작업을 해주며, contentType에 따른 최적화 방식이 다르며, 마지막에 optimize된 buffer를 반환합니다.

optimizeImage

imageOptimizer

이 함수는 위에서 확인했던 optimizeImage 함수를 사용하여 이미지를 최종적으로 마지막으로 최적화 시켜서 반환하는 함수입니다. (next server에서 사용됨)

imageOptimizer

  • imageUpstream와 paramsResult 인자를 통해 href, quality, width, mimeType, upstreamBuffer, maxAge, upstreamType 정보를 얻습니다. (이 코드에서는 생략되어있음)
  • optimizeImage 함수를 사용하여 optimizedBuffer 결과값을 가집니다.
  • 이 결과값을 기반으로 메타 데이터, 옵션 값, contentType을 설정하고 이를 반환시켜줍니다.

예외)

image의 타입이 animation이거나 vector인 경우에는 최적화가 되지않고 원본 이미지가 반환됩니다.
예외케이스

생각보다 꽤 많은 양의 코드를 확인해 보았는데요, next/image의 최적화가 어떻게 이루어지는지에 대한 근본적인 리서치로 조금 더 동작원리에 대해 이해할 수 있게 되었습니다.

참고
https://nextjs.org/docs/pages/building-your-application/optimizing/images
https://github.com/vercel/next.js/blob/canary/packages/next/src/client/image-component.tsx

0개의 댓글