이미지 Pixel Mosaic 애니메이션 — Lazy Loading 플러그인화
2년 전 프로토타입을 웹 접근성·성능 최적화·움직이는 이미지까지 고려한 JavaScript 모듈로 확장하기
2024년 4월, 이미지가 큰 픽셀에서 시작해 점차 선명해지는 Pixel Mosaic 애니메이션 프로토타입을 만들었습니다.
당시에는 CSS의 image-rendering: pixelated 속성을 이용해 크기가 다른 이미지 세 장을 순서대로 교체하는 정도로 구현했습니다. 50×28, 100×56, 640×360 크기의 이미지를 미리 준비한 뒤, 각 이미지를 원본 크기로 확대해 큰 픽셀에서 작은 픽셀로 변하는 것처럼 보여주는 방식이었습니다.
보이는 결과는 나쁘지 않았지만 실제 웹사이트에 적용하기에는 한계가 명확했습니다.
이미지마다 여러 크기의 파일을 미리 만들어야 했고, 이미지 크기와 비율이 달라지면 다시 작업해야 했습니다. 불러와야 하는 파일 수도 늘어나기 때문에 지연 로딩을 위한 기능이라기보다 단순한 전환 효과에 가까웠습니다.
당시 글의 마지막에는 이런 가설을 남겼습니다.
하나의 원본 이미지를 Canvas에서 작게 축소한 뒤 다시 확대하면,
별도의 저해상도 이미지를 만들지 않고도 Pixel Mosaic 효과를 적용할 수 있지 않을까?
JavaScript와 Canvas에 익숙하지 않았던 저는 AI의 도움을 받아 구현을 시도했습니다. 하지만 당시에는 제가 원하는 구조를 정확히 설명해도 가우시안 블러를 사용하거나, 픽셀을 불필요하게 복잡한 방식으로 다시 계산하는 코드가 만들어졌습니다.
imageSmoothingEnabled = false를 사용하면 비교적 단순하게 구현할 수 있다는 방향까지는 찾았지만, 여러 이미지에 안정적으로 적용할 수 있는 코드로 완성하지 못하고 프로토타입에서 멈췄습니다.
2년 뒤, 다시 꺼낸 프로토타입
그동안 AI를 활용한 개발 환경은 많이 달라졌습니다.
단순히 한 번의 프롬프트로 코드를 생성하는 수준을 넘어, 요구사항을 기능 단위로 나누고 실제 동작을 확인하면서 오류를 수정하거나 구조를 다시 설계하는 작업까지 함께 진행할 수 있게 됐습니다.
이번에는 단순히 화면에서 비슷하게 움직이는 결과를 만드는 것이 아니라,
실제 웹페이지에 적용할 수 있는 플러그인을 목표로 했습니다.
결과물은 Pixel Mosaic Lazy Loader라는 의존성 없는 JavaScript 모듈로 정리했습니다.
이 글을 작성하는 시점의 버전은 v1.2.6입니다. 여러 <img> 요소를 감지해 큰 픽셀에서 작은 픽셀을 거쳐 원본 이미지로 전환하며, 별도의 저해상도 이미지를 준비하지 않고 기존 원본 이미지를 Canvas에서 축소한 뒤 확대하는 방식으로 동작합니다.
별도의 저해상도 이미지를 만들지 않습니다
기존 프로토타입에서는 단계마다 별도의 이미지 파일이 필요했습니다.
새로운 플러그인은 현재 페이지에 있는 원본 <img>를 Canvas의 이미지 소스로 사용합니다. 이미지를 작은 크기로 축소한 뒤 imageSmoothingEnabled = false 상태로 다시 확대해 픽셀 모자이크를 만듭니다.
따라서 이미지 한 장을 위해 여러 해상도의 파일을 따로 제작하거나 서버에서 미리 생성할 필요가 없습니다.
이미지 크기에 따라 픽셀 단계를 자동으로 계산할 수도 있고, 필요한 경우 아래처럼 픽셀 크기를 직접 지정할 수도 있습니다.
PixelMosaic.init({
duration: 1600,
steps: [64, 40, 24, 14, 8, 4, 2]
});전환 시간, 시작 지연, 단계 수, 픽셀 크기와 노이즈 강도는 전체 설정 또는 이미지별 속성으로 조절할 수 있습니다.
화면에 들어오는 이미지부터 실행합니다
기본 설정에서는 이미지가 화면 근처에 들어왔을 때 효과를 시작합니다.
초기 문서에 포함된 이미지뿐 아니라 이후 DOM에 추가된 이미지도 감지할 수 있으며, 여러 이미지가 동시에 화면에 나타나더라도 한 번에 처리하는 수를 제한할 수 있습니다.
엄밀히 말하면 이 플러그인은 이미지의 네트워크 요청 방식을 새롭게 대체하는 라이브러리라기보다, 브라우저의 네이티브 지연 로딩과 함께 사용할 수 있는 시각적 이미지 로더에 가깝습니다.
<img
data-pixel-mosaic
loading="lazy"
src="/images/cover.jpg"
width="1200"
height="800"
alt="서비스 소개 화면"
/>loading="lazy"가 이미지 요청 시점을 관리하고, Pixel Mosaic Lazy Loader는 이미지가 준비돼 화면에 나타나는 과정을 픽셀 전환으로 표현합니다.
움직이는 이미지도 멈추지 않도록
처음 프로토타입을 만들었을 때 의외로 정적인 이미지보다 GIF에 적용한 결과가 더 재미있었습니다.
그래서 이번 플러그인에서는 GIF뿐 아니라 Animated WebP와 APNG도 고려했습니다.
지원되는 환경에서는 WebCodecs의 ImageDecoder를 이용해 애니메이션의 프레임과 재생 시간을 읽고, 각 프레임에 픽셀 모자이크를 적용합니다.
브라우저가 ImageDecoder를 지원하지 않거나 CORS 문제로 이미지 데이터를 읽을 수 없는 경우에도 애니메이션을 정지된 이미지로 바꾸지 않습니다. 원본 애니메이션의 재생을 유지한 채 픽셀 Canvas를 겹치는 방식으로 기능을 축소합니다.
애니메이션을 완벽하게 처리하지 못하는 환경에서도 최소한 원본 이미지의 움직임은 보존하는 방향을 선택했습니다.
웹 접근성을 해치지 않는 구조
시각적인 효과를 추가하더라도 기존 이미지의 의미와 조작을 방해해서는 안 된다고 생각했습니다.
플러그인은 원본 <img> 요소를 다른 요소로 교체하지 않습니다. 이미지의 alt 속성과 문서 구조를 그대로 유지하고, 효과를 위해 생성되는 Canvas만 보조 오버레이로 사용합니다.
Canvas에는 다음과 같은 속성이 적용됩니다.
aria-hidden="true"role="presentation"pointer-events: none
따라서 스크린 리더에 불필요한 요소로 노출되지 않으며, 이미지가 링크 안에 있더라도 클릭이나 키보드 포커스를 가로채지 않습니다.
운영체제나 브라우저에서 동작 줄이기를 설정한 사용자를 위해 prefers-reduced-motion: reduce도 반영합니다. 동작 줄이기가 활성화돼 있으면 모자이크와 노이즈 효과를 생략하고 원본 이미지를 바로 표시합니다.
모든 환경에서 똑같이 동작할 필요는 없습니다
처음에는 모든 브라우저와 기기에서 동일한 효과를 보여주는 것을 목표로 생각했습니다.
하지만 기능을 추가할수록 모든 환경에 같은 처리를 강제하는 것보다, 사용할 수 있는 기술과 기기 성능에 따라 적절하게 기능을 조절하는 편이 더 현실적이었습니다.
현재 플러그인은 환경에 따라 다음과 같이 동작합니다.
Canvas 2D와 ImageDecoder를 지원하는 환경
정적 이미지와 움직이는 이미지에 픽셀 모자이크와 실시간 노이즈를 적용합니다.Canvas 2D를 지원하는 환경
정적 이미지에는 픽셀 모자이크를 적용하고, 움직이는 이미지는 원본 재생을 유지합니다.Canvas를 사용할 수 없는 환경
CSS 또는 Web Animations API를 이용한 단계형 블러와 투명도 전환으로 대체합니다.JavaScript가 실행되지 않거나 동작 줄이기가 설정된 환경
원본<img>를 그대로 표시합니다.
최신 기능을 지원하는 환경에서는 효과를 확장하고, 그렇지 않은 환경에서는 콘텐츠를 훼손하지 않는 범위에서 자연스럽게 기능을 줄입니다.
웹에서 말하는 점진적 향상(Progressive Enhancement)과 우아한 기능 저하(Graceful Degradation)를 이번 작업을 통해 조금 더 구체적으로 이해하게 됐습니다.
모바일과 저사양 기기를 위한 성능 조절
Canvas와 실시간 노이즈는 여러 이미지에서 동시에 실행하면 생각보다 많은 자원을 사용할 수 있습니다.
따라서 플러그인은 Save-Data, deviceMemory, hardwareConcurrency와 포인터 유형 등을 참고해 렌더링 강도를 자동으로 조절할 수 있도록 구성했습니다.
기기 환경에 따라 동시에 처리할 이미지 수, Canvas의 DPR, 렌더링 FPS와 노이즈 프레임을 제한합니다.
PixelMosaic.init({
quality: "auto",
maxConcurrent: 2,
maxDpr: 2,
renderFps: "auto"
});성능이 낮은 기기에서는 모든 효과를 억지로 유지하기보다 동시 처리량과 해상도를 낮추고, 필요한 경우 애니메이션 프레임 디코딩을 생략합니다.
다만 성능을 고려한 제어 장치를 포함했다는 것이 모든 페이지에서 성능 향상을 보장한다는 의미는 아닙니다. 실제 적용 시에는 이미지 수와 크기, 페이지 구조와 대상 기기를 기준으로 직접 확인하는 과정이 필요합니다.
간단하게 적용하기
CSS와 JavaScript 파일을 불러온 뒤, 효과를 적용할 이미지에 data-pixel-mosaic 속성을 추가합니다.
<link rel="stylesheet" href="/assets/pixel-mosaic.css" />
<img
data-pixel-mosaic
loading="lazy"
src="/images/hero.jpg"
width="1280"
height="720"
alt="제품 대시보드 화면"
/>
<script src="/assets/pixel-mosaic.js"></script>
<script>
PixelMosaic.init({
duration: 1400,
steps: "auto",
stepCount: 8,
noise: {
enabled: true,
opacity: 0.14
}
});
</script>플러그인을 자동 감지 방식으로 설정하면 특정 영역의 이미지에 속성을 하나씩 추가하지 않고도 적용할 수 있습니다. 반대로 로고나 아이콘처럼 효과가 필요하지 않은 이미지에는 data-no-pixel-mosaic 속성을 지정해 제외할 수 있습니다.
자세한 옵션과 사용 방법은 GitHub 저장소의 README에서 확인할 수 있습니다.
AI가 프로토타입과 결과물 사이를 좁혀준 과정
2024년의 AI는 제가 원하는 효과와 구현 원리를 설명해도 실제로 사용할 수 있는 구조까지 만들어주지는 못했습니다.
2년 뒤에는 같은 아이디어를 바탕으로 정적 이미지와 움직이는 이미지의 처리 방식을 나누고, 브라우저 지원 범위와 CORS 문제를 확인하고, 접근성과 성능 조건에 따라 기능이 축소되는 구조까지 함께 검토할 수 있었습니다.
물론 AI가 처음부터 완성된 답을 내놓은 것은 아닙니다.
모자이크가 끝난 뒤 노이즈가 남는 문제, 움직이는 이미지가 정지되는 문제, Canvas가 이미지의 둥근 모서리를 벗어나는 문제처럼 실제로 실행해 봐야 발견할 수 있는 오류가 계속 발생했습니다.
각 문제를 직접 확인하고 요구사항을 다시 정의한 뒤, 코드를 수정하고 여러 이미지 포맷에서 반복해서 테스트하는 과정이 필요했습니다.
AI가 대신 완성해 준 결과라기보다, 예전에는 구현 능력의 한계로 포기했던 아이디어를 다시 꺼내 실제 결과물까지 발전시킬 수 있는 도구가 생겼다는 쪽에 가깝습니다.
마무리
2년 전에 만들었던 Pixel Mosaic는 정해진 이미지 세 장을 순서대로 교체하는 작은 프로토타입이었습니다.
당시 글에는 언젠가 하나의 원본 이미지와 Canvas만으로 여러 이미지에 적용해 보고 싶다는 내용을 적었습니다. 그때는 구현하지 못했지만, 이번에는 실제 웹페이지에서 사용할 수 있는 JavaScript 모듈로 다시 만들었습니다.
아직 복잡한 transform, 다중 clip-path, 교차 출처 애니메이션의 프레임 디코딩처럼 환경에 따라 제한되는 부분은 남아 있습니다. 브라우저와 사이트 구조가 달라지면 예상하지 못한 문제가 발생할 수도 있습니다.
그래도 단순히 비슷하게 보이는 데모에서 끝내지 않고, 웹 표준과 접근성을 유지하면서 다양한 환경에서 가능한 만큼 동작하는 도구로 발전시켰다는 점에 의미를 두고 싶습니다.








