정처기를 준비하게 된 이유는 비전공자로서 프론트엔드에 대해 공부할 때, 생각보다 모르는 용어가 많다는 느낌을 받았기 때문입니다. 물론 모르는게 생길때마다 이를 공부할 수도 있지만, 디자인 패턴등을 공부하면서 느낀점이 몰랐고 겪지 않았기 때문에 불편함을 느끼지 못했고, 개선하지 못하는 경우가 많다는 것이었습니다. 따라서 전반적인 소프트웨어적인 지식을 갈고 닦고자 정보처리기사 시험을 준비하게 되었습니다.
또한 이를 바탕으로 비전공자에서 오는 일종의 컴플렉스 또한 극복하고 싶었습니다.
준비하며
사실 시험을 준비하면서 좋았던 점도 많고, 제 생각과 다른점도 많았습니다. 면접 질문으로서만 공부했던 TCP/IP와 UDP에 대해서 조금은 더 자세히 알 수 있었고, metadata등 막연하게 사용하던 용어에 대해서도 좀 더 정확하게 알 수 있었습니다. 주로 사용하던 패턴이외에 다양한 디자인 패턴에 대해서도 맛 볼 수 있었습니다.
물론 해당 내용들이 깊지는 않았지만 이러한 것들이 있고, 이 용어들이 여기서 유래되었구나 하는 사실들을 알 수 있었습니다.
아쉬운 점은 시험 자체는 합격을 기준으로 봤을 떄, 언어에 많이 치우쳐 있었습니다. 그리고 그 속에서 C, 자바, 파이썬의 비중은 높았지만 자바스크립트의 비중이 굉장히 낮다는 사실을 확인했습니다. 프론트엔드 개발자로서 자바스크립트를 가장 큰 비중에 두고 학습했지만, 소프트웨어쪽에서는 작은 비중을 차지하는 언어구나 라는 생각이 들었습니다.
시험 준비 과정
애초에 목적이 소프트웨어적 지식 함양과 비전공자로서의 자격지심을 어느 정도 해결할 수 있는 자격증의 획득 2가지 였습니다. 따라서 인터넷에 나오는 시험준비 기간 (필기 1주 , 실기 2~3주) 보다는 훨씬 넉넉하게 투자하였습니다.
실기 6주, 필기 3주 정도 투자하였고, 문제집은 시나공을 활용하였습니다.
1회독을 끝내고, 기출 문제를 푼 후 틀린 단원을 다시 공부하였고 마지막 시험 직전 3일에는 시험에 주로 기출되는 단원 위주로 학습하였습니다.
시험 결과
남들보다 시간을 좀 더 투자해서인지 시험은 한번에 모두 합격하게 되었습니다.
단순히 시험 합격만 생각한다면 2,4,7,8,9,10,11만 공부해도 되겠다는 생각이 들었습니다. 시험 전날에 유튜브를 통해서 C언어와 자바에 대한 문제 풀이에 대해서 공부했던 것도 많이 도움이 되었고 단답형 문제에서도 아는게 많이 나와서 합격할 수 있었다고 생각합니다.
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(',')}.`
)
}
}
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:Preventlayout shiftautomatically 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~~ 와 같이 나오는 이유도 유추가 가능하네요.
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라는 경로가 나오고, 디바이스 사이즈들은 어떤걸 다루는 지, 이미지 포멧 등등..)
앞서 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가 방지가 됩니다.
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. 정리하기엔 너무 방대한 코드
연계되어있는 기능들이 너무 많아서 한 블로그 포스팅에 정리하기엔 너무 많았습니다. 간략하게 다루면서 생략된 내용들이 조금 아쉬운 거 같습니다.