728x90

준비하게 된 계기

 

프론트엔드를 공부하다 정보처리기사를 준비하게 되었습니다. 

정처기를 준비하게 된 이유는 비전공자로서 프론트엔드에 대해 공부할 때, 생각보다 모르는 용어가 많다는 느낌을 받았기 때문입니다. 물론 모르는게 생길때마다 이를 공부할 수도 있지만, 디자인 패턴등을 공부하면서 느낀점이 몰랐고 겪지 않았기 때문에 불편함을 느끼지 못했고, 개선하지 못하는 경우가 많다는 것이었습니다. 따라서 전반적인 소프트웨어적인 지식을 갈고 닦고자 정보처리기사 시험을 준비하게 되었습니다.

또한 이를 바탕으로 비전공자에서 오는 일종의 컴플렉스 또한 극복하고 싶었습니다.

준비하며

사실 시험을 준비하면서 좋았던  점도 많고, 제 생각과 다른점도 많았습니다. 면접 질문으로서만 공부했던 TCP/IP와 UDP에 대해서 조금은 더 자세히 알 수 있었고, metadata등 막연하게 사용하던 용어에 대해서도 좀 더 정확하게 알 수 있었습니다. 주로 사용하던 패턴이외에 다양한 디자인 패턴에 대해서도 맛 볼 수 있었습니다.

물론 해당 내용들이 깊지는 않았지만 이러한 것들이 있고, 이 용어들이 여기서 유래되었구나 하는 사실들을 알 수 있었습니다.

 

아쉬운 점은 시험 자체는 합격을 기준으로 봤을 떄, 언어에 많이 치우쳐 있었습니다. 그리고 그 속에서 C, 자바, 파이썬의 비중은 높았지만 자바스크립트의 비중이 굉장히 낮다는 사실을 확인했습니다. 프론트엔드 개발자로서 자바스크립트를 가장 큰 비중에 두고 학습했지만, 소프트웨어쪽에서는 작은 비중을 차지하는 언어구나 라는 생각이 들었습니다.

 

시험 준비 과정

애초에 목적이 소프트웨어적 지식 함양과 비전공자로서의 자격지심을 어느 정도 해결할 수 있는 자격증의 획득 2가지 였습니다. 따라서 인터넷에 나오는 시험준비 기간 (필기 1주 , 실기 2~3주) 보다는 훨씬 넉넉하게 투자하였습니다.

실기 6주, 필기 3주 정도 투자하였고, 문제집은 시나공을 활용하였습니다. 

 

 

1회독을 끝내고, 기출 문제를 푼 후 틀린 단원을 다시 공부하였고 마지막 시험 직전 3일에는 시험에 주로 기출되는 단원 위주로 학습하였습니다.

 

 

시험 결과

남들보다 시간을 좀 더 투자해서인지 시험은 한번에 모두 합격하게 되었습니다.

단순히 시험 합격만 생각한다면 2,4,7,8,9,10,11만 공부해도 되겠다는 생각이 들었습니다. 시험 전날에 유튜브를 통해서 C언어와 자바에 대한 문제 풀이에 대해서 공부했던 것도 많이 도움이 되었고 단답형 문제에서도 아는게 많이 나와서 합격할 수 있었다고 생각합니다.

 

 

 

'후기' 카테고리의 다른 글

fastcampus 3월 캠프콘- 1:1 커피챗 후기  (0) 2024.04.19
패스트캠퍼스 3월 CampCON  (0) 2024.03.26
728x90

들어가며

Next.js의 next/image 컴퍼넌트는 굉장히 많은 역할을 해줍니다.  보통  Image를 사용할 떄는 아래와 같이 width, height을지정해주고 사용합니다. 이후 화면이나 외부 컨테이너가 작아짐에 따라서 이미지의 크기가 줄어듭니다.

import Image from 'next/image'
 
export default function Page() {
  return (
    <Image
      src="/profile.png"
      width={500}
      height={500}
      alt="Picture of the author"
    />
  )
}

 

하지만 이번 프로젝트에서는 이미지의 크기가 외부 영역에 영향을 받게 되었습니다. 

이때 사용할 수 있는 속성이 fill입니다.

 

Next/Image Fill속성 사용하기

정확하게 크기를 정의할 수는 없지만, 외부 영역 기준으로 가득차게 할 때는 fill을 사용할 수 있습니다.

공식문서  Responsive Image with Fill  

import Image from 'next/image'
 
export default function Page({ photoUrl }) {
  return (
    <div style={{ position: 'relative', width: '300px', height: '500px' }}>
      <Image
        src={photoUrl}
        alt="Picture of the author"
        sizes="300px"
        fill
        style={{
          objectFit: 'contain',
        }}
      />
    </div>
  )
}

 

예시를 봤을 때, fill를 사용할때는  3가지를 고려해야 합니다.

1. fill을 사용하면 Image컴퍼넌트가 absolute 포지션이기에 외부가 absolute, fiexd, relative중 하나여야 한다.

// 공식문서 내부의 : packages\next\src\client\image-component.tsx

	if (img.parentElement) {
          const { position } = window.getComputedStyle(img.parentElement)
          const valid = ['absolute', 'fixed', 'relative']
          if (!valid.includes(position)) {
            warnOnce(
              `Image with src "${origSrc}" has "fill" and parent element with invalid "position". Provided "${position}" should be one of ${valid
                .map(String)
                .join(',')}.`
            )
          }
        }

 

2. sizes를 사용하여 최적화를 해야한다.

(사용하지 않으면 아래의 경고 문구가 나옵니다.) 

                `Image with src "${origSrc}" has "fill" but is missing "sizes" prop. Please add it to improve page performance. Read more: https://nextjs.org/docs/api-reference/next/image#sizes`
 

 

이전 포스트를 통해서 next.js의 image 가 html의 img태그를 사용하고 있음을 알 수 있었고, srcset과 preload를 사용하여 최적화를 시도하고 있음을 확인했습니다.

mdn 에서 img태그의 sizes를 확인해보면 뷰포인트 크기를 활용하여 반응형으로 소스크기를 적용할 수 있습니다.

width가 정해지지 않은 경우에는 sizes옵션을 주어서 viewport에 따라서 소스의 크기를 결정할 수 있고, 최적화가 가능합니다. 

 

저는 화면이 1000px보다 작은 경우에는 400px, 클 경우 200px로 설정하였습니다.

	 <Image
          src={src}
          alt={alt}
          fill
          sizes="(max-width: 1000px) 400px, 200px"
  	  />

 

 

3. style에서 objectFit 속성 사용

fill을 사용하더라도 이미지의 비율이 유지되기를 생각할 것입니다. 이떄 사용할 수 있는 것이 objectFit이고 이는 12버전까지 next/image에 포함되어  있는 기능이었습니다.

 

 

하지만 현재에는 deprecated 됐으며 공식문서 예시와 같이 css에서 관리해야 합니다.

 

https://nextjs.org/docs/pages/api-reference/components/image-legacy#comparison

 

 

next.js에서  style이나 className등을 사용한 css의 영역을 지지하여  관심사가 분리되었기 때문입니다.

 

 

 

참고 문헌

공식 문서 Image 컴퍼넌트 : https://nextjs.org/docs/app/api-reference/components/image

 

Components: <Image> | Next.js

Optimize Images in your Next.js Application using the built-in `next/image` Component.

nextjs.org

 

 

mdn img태그 

https://developer.mozilla.org/ko/docs/Web/HTML/Element/img

 

<img>: 이미지 삽입 요소 - HTML: Hypertext Markup Language | MDN

HTML <img> 요소는 문서에 이미지를 넣습니다.

developer.mozilla.org

https://nextjs.org/docs/pages/api-reference/components/image-legacy

 

Components: <Image> (Legacy) | Next.js

Backwards compatible Image Optimization with the Legacy Image component.

nextjs.org

 

728x90

들어가며

Next.js에서 Image를 제공하는데, 이를 통해서 이미지에 대한 최적화 작업을 간단히 할 수 있습니다. 

 Next.js 공식문서에서는  크기 최적화, 시각 안정성, 빠른 페이지 리로드, 자산의 유연성 4가지 측면에서 장점을 제시합니다. 

  • Size Optimization: Automatically serve correctly sized images for each device, using modern image formats like WebP and AVIF.
  • Visual Stability: Prevent layout shift automatically when images are loading.
  • Faster Page Loads: Images are only loaded when they enter the viewport using native browser lazy loading, with optional blur-up placeholders.
  • Asset Flexibility: On-demand image resizing, even for images stored on remote servers

하지만 개발에서 은탄환은 없다고 많이 말합니다. 따라서 어떤 기능을 어떻게 제공하는지에 대해서 알 필요성을 느꼈고, 이를 위해서 소스코드를 분석을 하게 되었습니다.

 

소스코드 위치 추적

 리액트 기반의 UI 라이브러리나 react/drei 등 코드를 본적이 종종 있었는데,  개인적으로 파일이 너무 많고 복잡해서 파악하는데 조금 더 어려웠습니다. VS-CODE의 도움을 받고자 클론을 받고 파일을 경로를 추적했습니다.

 

우리가 사용하고 있는 next/image는 image-external에서 가져오는 군요

import type { ImageConfigComplete, ImageLoaderProps } from './image-config'
import type { ImageProps, ImageLoader, StaticImageData } from './get-img-props'

import { getImgProps } from './get-img-props'
import { Image } from '../../client/image-component'

// @ts-ignore - This is replaced by webpack alias
import defaultLoader from 'next/dist/shared/lib/image-loader'

/**
 * For more advanced use cases, you can call `getImageProps()`
 * to get the props that would be passed to the underlying `<img>` element,
 * and instead pass to them to another component, style, canvas, etc.
 *
 * Read more: [Next.js docs: `getImageProps`](https://nextjs.org/docs/app/api-reference/components/image#getimageprops)
 */
export function getImageProps(imgProps: ImageProps) {
  const { props } = getImgProps(imgProps, {
    defaultLoader,
    // This is replaced by webpack define plugin
    imgConf: process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete,
  })
  // Normally we don't care about undefined props because we pass to JSX,
  // but this exported function could be used by the end user for anything
  // so we delete undefined props to clean it up a little.
  for (const [key, value] of Object.entries(props)) {
    if (value === undefined) {
      delete props[key as keyof typeof props]
    }
  }
  return { props }
}

export default Image

export type { ImageProps, ImageLoaderProps, ImageLoader, StaticImageData }

우리가 사용하고 있는 next/image는 image-external에서 가져오는 군요

Image 이외에도 getImageProps와  ImageLoader등 몇 가지를 추가적으로 export하고 있네요.

 

사실 Image에 대해서 처음 관심을 갖게 된것은 wanted에서 멘토님이 defaultLoader와 관련된 설명을 해주시면서 분석해야겠다는 생각을 하게 되었습니다. Next.js에서는 defaultLoader를 사용하여 서버에서 캐싱을 해주는데, 이와 관련된 작업이 서버에 부담이 크다는 discussion을 보여주면서 설명했던 내용입니다.

next.js image

 

이때 defaultLoader의 props 타입을 보면 next.js에서가 어떤식으로 srcset을 관리하는지 유추할 수 있었습니다.

흔히 next.js에서는 webp로 관리한다고 하고 경로가 /_next/image~~ 와 같이 나오는 이유도 유추가 가능하네요.

function defaultLoader({
  config,
  src,
  width,
  quality,
}: ImageLoaderPropsWithConfig): string {
	// ..생략
}

export type ImageLoaderPropsWithConfig = ImageLoaderProps & {
  config: Readonly<ImageConfig>
}

export const imageConfigDefault: ImageConfigComplete = {
  deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  path: '/_next/image',
  loader: 'default',
  loaderFile: '',
  domains: [],
  disableStaticImages: false,
  minimumCacheTTL: 60,
  formats: ['image/webp'],
  dangerouslyAllowSVG: false,
  contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`,
  contentDispositionType: 'attachment',
  remotePatterns: [],
  unoptimized: false,
}

 

 

 

소스코드 분석

이제 이미지 컴퍼넌트에 대해서 좀 더 집중해보려고 합니다. 경로는 packages/next/src/client/image-component.tsx 입니다

파일 자체는 타입이나 export하는 과정만 50줄에 이르고  코드만 430줄이 넘더군요.. 이를 topdown 방식으로 분석해보려고 합니다.

 Image 

export const Image = forwardRef<HTMLImageElement | null, ImageProps>(
  (props, forwardedRef) => {
    const pagesRouter = useContext(RouterContext)
    // We're in the app directory if there is no pages router.
    const isAppRouter = !pagesRouter

    const configContext = useContext(ImageConfigContext)
    const config = useMemo(() => {
      const c = configEnv || configContext || imageConfigDefault
      const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
      const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
      return { ...c, allSizes, deviceSizes }
    }, [configContext])

    const { onLoad, onLoadingComplete } = props
    const onLoadRef = useRef(onLoad)

    useEffect(() => {
      onLoadRef.current = onLoad
    }, [onLoad])

    const onLoadingCompleteRef = useRef(onLoadingComplete)

    useEffect(() => {
      onLoadingCompleteRef.current = onLoadingComplete
    }, [onLoadingComplete])

    const [blurComplete, setBlurComplete] = useState(false)
    const [showAltText, setShowAltText] = useState(false)

    const { props: imgAttributes, meta: imgMeta } = getImgProps(props, {
      defaultLoader,
      imgConf: config,
      blurComplete,
      showAltText,
    })

    return (
      <>
        {
          <ImageElement
            {...imgAttributes}
            unoptimized={imgMeta.unoptimized}
            placeholder={imgMeta.placeholder}
            fill={imgMeta.fill}
            onLoadRef={onLoadRef}
            onLoadingCompleteRef={onLoadingCompleteRef}
            setBlurComplete={setBlurComplete}
            setShowAltText={setShowAltText}
            sizesInput={props.sizes}
            ref={forwardedRef}
          />
        }
        {imgMeta.priority ? (
          <ImagePreload
            isAppRouter={isAppRouter}
            imgAttributes={imgAttributes}
          />
        ) : null}
      </>
    )
  }
)

 

우선 useContext를 통해서 페이지 라우터여부, imageConfig 설정을 받아옵니다.

이후 환경변수 ,context, 기본설정 순으로 존재 여부 확인 후 적용할 config를 결정합니다.

그리고 props중에 유저가 설정한 onLoaded와 onLoadedComplete를 구조분해할당합니다.

 

이후 아래와 같이 getImgProps 함수에 할당해 줍니다.  ( getImageProps 에도 사용되는 공용함수)

    const { props: imgAttributes, meta: imgMeta } = getImgProps(props, {
      defaultLoader,
      imgConf: config,
      blurComplete,
      showAltText,
    })

 

네이밍에서도 알 수 있듯이 이미지 등록에 필요한 과정들과 이를 바탕으로 next.js의 이미지 컴퍼넌트에 사용할 props를 반환해 줍니다.

 

이후  이미지 요소와 preLoad요소를 반환된 설정에 맞춰서  보여줍니다.

따라서 어떤 요소를 반환하는 함수인지에 대해서 먼저 확인해보려고 합니다.

 

getImgProps

400줄에 달하는 코드라서 한번에 다 보여주기 보다는 부분 부분 잘라서 설명할려고 합니다.( 내부에 사용하는 함수까지 합치면 600줄 )
코드 전문은 아래에서 확인해보실 수 있습니다.

https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/get-img-props.ts

 

next.js/packages/next/src/shared/lib/get-img-props.ts at canary · vercel/next.js

The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.

github.com

 

우선 매개변수와 반환값의 타입부터 확인하겠습니다

  const { props: imgAttributes, meta: imgMeta } = getImgProps(props, {
      defaultLoader,
      imgConf: config,
      blurComplete,
      showAltText,
    })

Image 컴퍼넌트에서는 위와 같이 getImgProps 함수를 사용했었습니다. img 태그에 사용할 설정과 image를 어떻게 상태관리할지에 대해서 2가지가 나옵니다. 그리고 반환값으로 

이중  onLoadingComplete, layout,  objectFit,  objectPosition, lazyBoundary,  lazyRoot는 더 이상 사용하지 않아서 봤더니 deprecated됐다고 하네요.

  {
    src,
    sizes,
    unoptimized = false,
    priority = false,
    loading,
    className,
    quality,
    width,
    height,
    fill = false,
    style,
    overrideSrc,
    onLoad,
    onLoadingComplete,
    placeholder = 'empty',
    blurDataURL,
    fetchPriority,
    layout,
    objectFit,
    objectPosition,
    lazyBoundary,
    lazyRoot,
    ...rest
  }: ImageProps,
  _state: {
    defaultLoader: ImageLoaderWithConfig
    imgConf: ImageConfigComplete
    showAltText?: boolean
    blurComplete?: boolean
  }
): {
  props: ImgProps
  meta: {
    unoptimized: boolean
    priority: boolean
    placeholder: NonNullable<ImageProps['placeholder']>
    fill: boolean
  }
} {

 

 

이후 img태그에 직접사용할 요소는 props로 그리고 meta데이터를 반환해줍니다.

다양한 조건문을 통한 에러검증이 내부에 존재하는데, 예외처리 및 props를 활용하여 img의 크기 등이 결정되면은

generateImgAttrs함수가 실행됩니다.

  const imgAttributes = generateImgAttrs({
    config,
    src,
    unoptimized,
    width: widthInt,
    quality: qualityInt,
    sizes,
    loader,
  })

 

function generateImgAttrs({
  config,
  src,
  unoptimized,
  width,
  quality,
  sizes,
  loader,
}: GenImgAttrsData): GenImgAttrsResult {
  if (unoptimized) {
    return { src, srcSet: undefined, sizes: undefined }
  }

  const { widths, kind } = getWidths(config, width, sizes)
  const last = widths.length - 1

  return {
    sizes: !sizes && kind === 'w' ? '100vw' : sizes,
    srcSet: widths
      .map(
        (w, i) =>
          `${loader({ config, src, quality, width: w })} ${
            kind === 'w' ? w : i + 1
          }${kind}`
      )
      .join(', '),

    // It's intended to keep `src` the last attribute because React updates
    // attributes in order. If we keep `src` the first one, Safari will
    // immediately start to fetch `src`, before `sizes` and `srcSet` are even
    // updated by React. That causes multiple unnecessary requests if `srcSet`
    // and `sizes` are defined.
    // This bug cannot be reproduced in Chrome or Firefox.
    src: loader({ config, src, quality, width: widths[last] }),
  }
}

해당 함수를 통해서 sizes와 srcSet, src가 결정됩니다.

srcSet 코드를 보면 loader를 활용하여 반환된 이름이 나오고 아래와 같이 이름이 나온 이유가 유추가능합니다.

next.js image

 

마지막으로 반환되는 내용들은 아래와 같습니다. imgAttributes에서 sizes, srcSet, src를 반환해줬는데 해당 값을 props에 다시 할당해줬네요. 

  const props: ImgProps = {
    ...rest,
    loading: isLazy ? 'lazy' : loading,
    fetchPriority,
    width: widthInt,
    height: heightInt,
    decoding: 'async',
    className,
    style: { ...imgStyle, ...placeholderStyle },
    sizes: imgAttributes.sizes,
    srcSet: imgAttributes.srcSet,
    src: overrideSrc || imgAttributes.src,
  }
  const meta = { unoptimized, priority, placeholder, fill }
  return { props, meta }
}

 

 

const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
  (
    {
      src,
      srcSet,
      sizes,
      height,
      width,
      decoding,
      className,
      style,
      fetchPriority,
      placeholder,
      loading,
      unoptimized,
      fill,
      onLoadRef,
      onLoadingCompleteRef,
      setBlurComplete,
      setShowAltText,
      sizesInput,
      onLoad,
      onError,
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <img
        {...rest}
        {...getDynamicProps(fetchPriority)}
        // It's intended to keep `loading` before `src` because React updates
        // props in order which causes Safari/Firefox to not lazy load properly.
        // See https://github.com/facebook/react/issues/25883
        loading={loading}
        width={width}
        height={height}
        decoding={decoding}
        data-nimg={fill ? 'fill' : '1'}
        className={className}
        style={style}
        // It's intended to keep `src` the last attribute because React updates
        // attributes in order. If we keep `src` the first one, Safari will
        // immediately start to fetch `src`, before `sizes` and `srcSet` are even
        // updated by React. That causes multiple unnecessary requests if `srcSet`
        // and `sizes` are defined.
        // This bug cannot be reproduced in Chrome or Firefox.
        sizes={sizes}
        srcSet={srcSet}
        src={src}
        ref={useCallback(
          (img: ImgElementWithDataProp | null) => {
            if (forwardedRef) {
              if (typeof forwardedRef === 'function') forwardedRef(img)
              else if (typeof forwardedRef === 'object') {
                // @ts-ignore - .current is read only it's usually assigned by react internally
                forwardedRef.current = img
              }
            }
            if (!img) {
              return
            }
            if (onError) {
              // If the image has an error before react hydrates, then the error is lost.
              // The workaround is to wait until the image is mounted which is after hydration,
              // then we set the src again to trigger the error handler (if there was an error).
              // eslint-disable-next-line no-self-assign
              img.src = img.src
            }
            if (process.env.NODE_ENV !== 'production') {
              if (!src) {
                console.error(`Image is missing required "src" property:`, img)
              }
              if (img.getAttribute('alt') === null) {
                console.error(
                  `Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines.`
                )
              }
            }
            if (img.complete) {
              handleLoading(
                img,
                placeholder,
                onLoadRef,
                onLoadingCompleteRef,
                setBlurComplete,
                unoptimized,
                sizesInput
              )
            }
          },
          [
            src,
            placeholder,
            onLoadRef,
            onLoadingCompleteRef,
            setBlurComplete,
            onError,
            unoptimized,
            sizesInput,
            forwardedRef,
          ]
        )}
        onLoad={(event) => {
          const img = event.currentTarget as ImgElementWithDataProp
          handleLoading(
            img,
            placeholder,
            onLoadRef,
            onLoadingCompleteRef,
            setBlurComplete,
            unoptimized,
            sizesInput
          )
        }}
        onError={(event) => {
          // if the real image fails to load, this will ensure "alt" is visible
          setShowAltText(true)
          if (placeholder !== 'empty') {
            // If the real image fails to load, this will still remove the placeholder.
            setBlurComplete(true)
          }
          if (onError) {
            onError(event)
          }
        }}
      />
    )
  }
)

 

defaultLoader의 코드를 자세히 분석하진 않을거지만 로더에 사용되는 Config값을 확인하면  Next.js  Size Optimization에 대해서 유추가능합니다. 왜 /_next/image라는 경로가 나오고, 디바이스 사이즈들은 어떤걸 다루는 지, 이미지 포멧 등등..) 

export const imageConfigDefault: ImageConfigComplete = {
  deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  path: '/_next/image',
  loader: 'default',
  loaderFile: '',
  domains: [],
  disableStaticImages: false,
  minimumCacheTTL: 60,
  formats: ['image/webp'],
  dangerouslyAllowSVG: false,
  contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`,
  contentDispositionType: 'attachment',
  remotePatterns: [],
  unoptimized: false,
}

 

Image의 JSX요소인 ImageElement와 ImagePreload

return (
      <>
        {
          <ImageElement
            {...imgAttributes}
            unoptimized={imgMeta.unoptimized}
            placeholder={imgMeta.placeholder}
            fill={imgMeta.fill}
            onLoadRef={onLoadRef}
            onLoadingCompleteRef={onLoadingCompleteRef}
            setBlurComplete={setBlurComplete}
            setShowAltText={setShowAltText}
            sizesInput={props.sizes}
            ref={forwardedRef}
          />
        }
        {imgMeta.priority ? (
          <ImagePreload
            isAppRouter={isAppRouter}
            imgAttributes={imgAttributes}
          />
        ) : null}
      </>
    )
  }

 

ImagePreload

앞서 meta의 반환값으로 getImgProps함수가 meta를 반환하고 있고, 이를 Image 컴퍼넌트에서는 imgMeta에 할당하여 상용합니다. 이중에서 priority가 체크된 이미지의 경우 Preload가 일어납니다. 이때 Approuter에서는 일어나고, ReactDom의 preload함수를 사용하고 pageRouter에서는 Head와 link태그를 활용해서 preload를 합니다.

function ImagePreload({
  isAppRouter,
  imgAttributes,
}: {
  isAppRouter: boolean
  imgAttributes: ImgProps
}) {
  const opts = {
    as: 'image',
    imageSrcSet: imgAttributes.srcSet,
    imageSizes: imgAttributes.sizes,
    crossOrigin: imgAttributes.crossOrigin,
    referrerPolicy: imgAttributes.referrerPolicy,
    ...getDynamicProps(imgAttributes.fetchPriority),
  }

  if (isAppRouter && ReactDOM.preload) {
    // See https://github.com/facebook/react/pull/26940
    ReactDOM.preload(
      imgAttributes.src,
      // @ts-expect-error TODO: upgrade to `@types/react-dom@18.3.x`
      opts
    )
    return null
  }

  return (
    <Head>
      <link
        key={
          '__nimg-' +
          imgAttributes.src +
          imgAttributes.srcSet +
          imgAttributes.sizes
        }
        rel="preload"
        // Note how we omit the `href` attribute, as it would only be relevant
        // for browsers that do not support `imagesrcset`, and in those cases
        // it would cause the incorrect image to be preloaded.
        //
        // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset
        href={imgAttributes.srcSet ? undefined : imgAttributes.src}
        {...opts}
      />
    </Head>
  )
}

 

 

ImageElement

 

const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
  (
    {
      src,
      srcSet,
      sizes,
      height,
      width,
      decoding,
      className,
      style,
      fetchPriority,
      placeholder,
      loading,
      unoptimized,
      fill,
      onLoadRef,
      onLoadingCompleteRef,
      setBlurComplete,
      setShowAltText,
      sizesInput,
      onLoad,
      onError,
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <img
        {...rest}
        {...getDynamicProps(fetchPriority)}
        // It's intended to keep `loading` before `src` because React updates
        // props in order which causes Safari/Firefox to not lazy load properly.
        // See https://github.com/facebook/react/issues/25883
        loading={loading}
        width={width}
        height={height}
        decoding={decoding}
        data-nimg={fill ? 'fill' : '1'}
        className={className}
        style={style}
        // It's intended to keep `src` the last attribute because React updates
        // attributes in order. If we keep `src` the first one, Safari will
        // immediately start to fetch `src`, before `sizes` and `srcSet` are even
        // updated by React. That causes multiple unnecessary requests if `srcSet`
        // and `sizes` are defined.
        // This bug cannot be reproduced in Chrome or Firefox.
        sizes={sizes}
        srcSet={srcSet}
        src={src}
        ref={useCallback(
          (img: ImgElementWithDataProp | null) => {
            if (forwardedRef) {
              if (typeof forwardedRef === 'function') forwardedRef(img)
              else if (typeof forwardedRef === 'object') {
                // @ts-ignore - .current is read only it's usually assigned by react internally
                forwardedRef.current = img
              }
            }
            if (!img) {
              return
            }
            if (onError) {
              // If the image has an error before react hydrates, then the error is lost.
              // The workaround is to wait until the image is mounted which is after hydration,
              // then we set the src again to trigger the error handler (if there was an error).
              // eslint-disable-next-line no-self-assign
              img.src = img.src
            }
            if (process.env.NODE_ENV !== 'production') {
              if (!src) {
                console.error(`Image is missing required "src" property:`, img)
              }
              if (img.getAttribute('alt') === null) {
                console.error(
                  `Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines.`
                )
              }
            }
            if (img.complete) {
              handleLoading(
                img,
                placeholder,
                onLoadRef,
                onLoadingCompleteRef,
                setBlurComplete,
                unoptimized,
                sizesInput
              )
            }
          },
          [
            src,
            placeholder,
            onLoadRef,
            onLoadingCompleteRef,
            setBlurComplete,
            onError,
            unoptimized,
            sizesInput,
            forwardedRef,
          ]
        )}
        onLoad={(event) => {
          const img = event.currentTarget as ImgElementWithDataProp
          handleLoading(
            img,
            placeholder,
            onLoadRef,
            onLoadingCompleteRef,
            setBlurComplete,
            unoptimized,
            sizesInput
          )
        }}
        onError={(event) => {
          // if the real image fails to load, this will ensure "alt" is visible
          setShowAltText(true)
          if (placeholder !== 'empty') {
            // If the real image fails to load, this will still remove the placeholder.
            setBlurComplete(true)
          }
          if (onError) {
            onError(event)
          }
        }}
      />
    )
  }
)

그리고 ref에서 loading을 통해 이미지 로드 완료시 블러처리 이벤트 전파 관리등의 추가작업을 실행하고 있습니다.

이때 getImgProps함수를 통해 반환했던 width값등이 지정되기 때문에 layoutShift가 방지가 됩니다.

https://web.dev/articles/optimize-cls?hl=ko

 

레이아웃 변경 누적 최적화  |  Articles  |  web.dev

레이아웃 변경 횟수 (CLS)는 사용자가 페이지 콘텐츠의 갑작스러운 변화를 경험하는 빈도를 정량화하는 측정항목입니다. 이 가이드에서는 크기 또는 동적 콘텐츠가 없는 이미지 및 iframe과 같이 C

web.dev

 

Asset Flexibility에 대해서는 찾아보지 못했지만, Next.js에서 어떻게 Size Optimization, Visual Stability, Faster Page Loads 를 하려고 했는지는 직접 확인할 수 있었습니다.

 

느낀점

1. 다양한 이슈를 확인하고 에러처리를 볼 수 있어서 좋았다.

대표적인게 getDynamicProps함수인데, next.js의 19버전과 이전 버전에서 옵션의 네이밍 컨벤션이 달라진걸 처리하는 함수이다.

function getDynamicProps(
  fetchPriority?: string
): Record<string, string | undefined> {
  if (Boolean(use)) {
    // In React 19.0.0 or newer, we must use camelCase
    // prop to avoid "Warning: Invalid DOM property".
    // See https://github.com/facebook/react/pull/25927
    return { fetchPriority }
  }
  // In React 18.2.0 or older, we must use lowercase prop
  // to avoid "Warning: Invalid DOM property".
  return { fetchpriority: fetchPriority }
}

 

2. Image컴퍼넌트와 img태그에 대한 학습

img.complete등 생각보다 모르는 내용들이 있었고, Image컴퍼넌트에 대해서도 몰라서 사용하지 않았던 내용들이 있었느데, 소스 코드를 보면서 알 수 있어서 좋았습니다.  개발 실력을 기르려면 오픈소스 코드를 많이 참고하라는 이유를 느낄 수 있었습니다.

 

3. 컴퍼넌트 분리 기준에 대한 호기심

 구성된 코드가 제 생각보다 굉장히 복잡했습니다. 그런데 생각보다 컴퍼넌트가 간략하게 분리되어있어서 놀랐습니다.

error처리가 함수로 분리된게 아닌 직접 적혀있었는데, 이게 라이브러리라서 이런건지 어떤 기준으로 분리를 한건지 개인적으로 호기심이 생겼습니다.

 

4. 네이밍이 좋아서 코드가 잘 읽힌다.

네이밍이 굉장히 좋다는 생각이 드는 부분도 있었고 getImageProps와 getImgProps처럼 애매한 네이밍도 보였다고 생각했느데, 나름의 합리성이 느껴지고 덕분에 코드가 읽기 더 수월했습니다. 좋은 변수명이 항상 어려운데 이런걸 보다 보면 늘지 않을까 라는 생각이 들었습니다.

 

5. 정리하기엔 너무 방대한 코드

연계되어있는 기능들이 너무 많아서 한 블로그 포스팅에 정리하기엔 너무 많았습니다. 간략하게 다루면서 생략된 내용들이 조금 아쉬운 거 같습니다.

 

 

 

참고한 링크

https://developer.mozilla.org/ko/docs/Web/HTML/Element/img

 

<img>: 이미지 삽입 요소 - HTML: Hypertext Markup Language | MDN

HTML <img> 요소는 문서에 이미지를 넣습니다.

developer.mozilla.org

 

https://github.com/vercel/next.js/tree/canary

 

GitHub - vercel/next.js: The React Framework

The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.

github.com

https://nextjs.org/docs/app/api-reference/components/image

 

Components: <Image> | Next.js

Optimize Images in your Next.js Application using the built-in `next/image` Component.

nextjs.org

https://web.dev/articles/optimize-cls?hl=ko

 

레이아웃 변경 누적 최적화  |  Articles  |  web.dev

레이아웃 변경 횟수 (CLS)는 사용자가 페이지 콘텐츠의 갑작스러운 변화를 경험하는 빈도를 정량화하는 측정항목입니다. 이 가이드에서는 크기 또는 동적 콘텐츠가 없는 이미지 및 iframe과 같이 C

web.dev

 

+ Recent posts