220326 Smooth Zoom 제작일지

개발

220326 Smooth Zoom 제작일지
최종 수정일:

번들러 추가

더는 수동으로 빌드하지 않아도 됩니다.
webpack과 rollup의 차이를 뼈에 새기는 계기가 되었네요.
관련해 글을 하나 작성해볼까 싶기도 했으나, 이미 한글로도 글이 꽤 많아 넘어갈 생각입니다.

빌드 자동화를 못 해 시작하기 꺼려지거나, 아예 시작조차 못 했던 많은 작업도 진행하게 됐습니다.

Typescript 도입

수동으로 빌드하던 시절에도 할 수는 있는 작업이었지만, 아무래도 일을 불필요하게 여러 번 해야 해서 꺼려지던 게 사실입니다.
rollup을 쓰는 지금은 TypeScript를 쓰지 말아야 할 이유가 하나도 없기에 곧바로 도입했습니다.

배포 자동화

Smooth Zoom 워크플로우

새 버전을 배포할 때마다 GitHub Actions로 자동으로 배포하도록 해뒀습니다.
번들러만 있으면 20줄 내외의 yml 파일로 할 수 있는 작업인데, 수동으로 하느라 많이 답답했습니다.

옵션 업데이트

originalizer 제거, onTransitionEnd 추가

트랜지션이 끝난 시점에 이미지의 src 속성만 수정할 수 있는 함수보단, 트랜지션이 끝난 시점에 이미지 자체를 수정할 수 있는 게 범용성이 좋을 것 같아 originalizer(src: string) => void를 제거하고 onTransitionEnd(img: HTMLImageElement) => void를 추가했습니다.

useMaximumSize 추가

onTransitionEnd 함수가 옵셔널인데 매번 srcset을 전부 뒤져 최대 크기로 확대하는 건 합리적이지 않은 것 같아 useMaximumSize 옵션을 추가했습니다.

최적화 및 리팩터링

확대할 비 구하기

let maxWidth = image.naturalWidth;
 
if (srcset) {
    const sizes = srcset.match(/ ([0-9]+)w/gm);
 
    if (sizes) {
        // Find image's largest width in 'srcset' attribute
        sizes.forEach((size) => {
            const sizeNum = +size.trim().replace("w", "");
 
            if (sizeNum > maxWidth) {
                maxWidth = sizeNum;
            }
        });
    }
}
 
const ratio = height / width;
 
// Image's width shouldn't be larger than screen width
if (maxWidth >= screenWidth) {
    maxWidth = screenWidth;
}
// And height too
const maxHeight = maxWidth * ratio;
 
if (maxHeight >= screenHeight) {
    maxWidth = (maxWidth * screenHeight) / maxHeight;
}
 
const scale = maxWidth !== width ? maxWidth / width : 1;

기존 코드입니다.

maxWidth 변수를 여기저기서 접근하는데다, 조건문이 너무 많고 심지어 저 코드가 여기저기 파편화돼있으니 모르는 사람이 보면 '뭐 어쩌라는 거지…'란 생각이 절로 들만 한 코드입니다.

const sizes = srcset.match(/ ([0-9]+)w/gm) || [];
const maxWidth = useMaximumSize
    ? Math.max(
          naturalWidth,
          ...sizes
              .map((x) => +x.trim().replace("w", ""))
              .filter((x) => !Number.isNaN(x) && naturalWidth < x)
      )
    : naturalWidth;
const imageScale = maxWidth / width;
const maxScale = Math.min(screenWidth / width, screenHeight / height);
const scale = Math.min(maxScale, imageScale);

Math.maxMath.min을 활용해 조건문을 없애고, 모든 값을 상수로 선언했습니다.

평균 색 구하기

const getAverageRGB = (img) => {
    const blockSize = 5;
    const rgb = { r: 0, g: 0, b: 0 };
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    if (!ctx) return null;
    const width = (canvas.width =
        img.naturalWidth || img.offsetWidth || img.width);
    const height = (canvas.height =
        img.naturalHeight || img.offsetHeight || img.height);
 
    let data;
    let i = -4;
    let length;
    let count = 0;
 
    ctx.drawImage(img, 0, 0);
 
    try {
        data = ctx.getImageData(0, 0, width, height);
    } catch (e) {
        return null;
    }
 
    length = data.data.length;
    while ((i += blockSize * 4) < length) {
        ++count;
        rgb.r += data.data[i];
        rgb.g += data.data[i + 1];
        rgb.b += data.data[i + 2];
    }
 
    rgb.r = Math.floor(rgb.r / count);
    rgb.g = Math.floor(rgb.g / count);
    rgb.b = Math.floor(rgb.b / count);
 
    return rgb;
};

Stackoverflow의 답변을 거의 붙여 넣은 수준의, 도대체 어디서부터 어디까지 지적해줘야 할지 모르겠는 수준의 코드였습니다.

function getAverageRGB(img: HTMLImageElement, width: number, height: number) {
    const blockSize = 20;
    const rgb = { r: 0, g: 0, b: 0 };
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const optimizedWidth = Math.sqrt(width);
    const optimizedHeight = Math.sqrt(height);
 
    if (!ctx) {
        return rgb;
    }
 
    canvas.width = optimizedWidth;
    canvas.height = optimizedHeight;
 
    try {
        ctx.drawImage(img, 0, 0, optimizedWidth, optimizedHeight);
 
        const imageData = ctx.getImageData(
            0,
            0,
            optimizedWidth,
            optimizedHeight
        );
 
        const { data } = imageData;
        const { length } = data;
        const count = length / blockSize;
 
        for (let i = 0; i < length; i += blockSize) {
            rgb.r += data[i];
            rgb.g += data[i + 1];
            rgb.b += data[i + 2];
        }
 
        rgb.r = Math.floor(rgb.r / count);
        rgb.g = Math.floor(rgb.g / count);
        rgb.b = Math.floor(rgb.b / count);
    } catch {
        return rgb;
    }
 
    return rgb;
}

코드를 수정함과 동시에, canvas의 크기도 수정했습니다.
기존엔 이미지와 같은 크기의 canvas를 탐색했는데, 이러면 이미지 크기가 조금만 커져도 루프를 도는데 한세월이 걸립니다. 제가 필요한 값은 '대충 이미지에 제일 많이 쓰여 몰입감을 해치지 않는 색'이기에, Math.sqrt로 canvas 크기를 줄였습니다.

무의미한 스타일 계산 제거

기존엔 transition의 적용을 위해 이미지를 DOM에 추가한 뒤, offsetWidth을 호출하거나, style을 변경해 스타일 재계산을 유발했습니다.

window.requestAnimationFrame(() => {
    imageClone.style.transform = `matrix(${scale}, 0, 0, ${scale}, ${wrapX}, ${wrapY})`;
    bg.classList.add("zoom-bg--reveal");
});

굳이 시간이 얼마나 걸릴지 모르는 불필요한 작업을 시키기보다, 다음 프레임에 style을 적용해주면 transition이 적용됩니다.

의외의 변화들은 커밋 메시지로 남겨두고 생략하겠습니다.

Report an issue