

2025년 오늘의집 리브랜딩 캠페인에서는 브랜드의 변화를 가장 먼저 보여주는 접점, 바로 ATF(Above the Fold) 영역에 특별한 인터랙션이 필요했습니다. 약 2~3주라는 짧은 개발기간 동안 수백 장의 이미지로 새로운 로고를 구성하고, 여기에 애니메이션 효과까지 결합하는 작업은 디자이너와 개발자 모두에게 결코 쉽지 않은 도전이었습니다. 본 글에서는 구현 난이도가 높은 ATF 영역의 인터랙션을 어떻게 기술적으로 풀어갔는지, 그 고민과 해결 여정을 공유드립니다.
왜 이 인터랙션은 어려웠을까?
ATF는 사용자가 페이지에 진입하는 순간 가장 먼저 보게 되는 영역입니다. 이곳에서의 렌더링 성능은 전체 사용자 경험에 직접적인 영향을 미치며, 높은 지연시간은 사용자 이탈로 이어질 수 있습니다. 이번 프로젝트에서는 다음과 같은 조건들이 동시에 충족되어야 했습니다.
- 약 500장 이상의 이미지가 한 화면에서 동적으로 배치·이동해야 함
- 스크롤과 프레임 단위로 연동되는 정교한 애니메이션 요구
- PC·모바일 모두에서 대량의 리소스 다운로드 최적화 및 렌더링 퍼포먼스를 보장하는 높은 성능 기준이 요구됨
이런 요구사항들은 단순히 ‘많은 이미지를 그리는 것’ 이상의 문제였습니다. 웹 브라우저가 가진 구조적 제약(렌더링 파이프라인, 네트워크 요청 개수, 디코딩 비용)을 정면으로 다뤄야 했기 때문입니다.

문제의 정의 : 어디서부터 어떻게 풀어야 할까?
개발자로서 가장 먼저 든 생각은 "이걸 진짜 구현할 수 있을까?"였습니다. 전체 요구사항을 하나의 기능 단위로 바라보면 해결해야 할 문제가 명확하게 보이지 않았기 때문입니다.
그래서 문제를 구성 요소 단위로 다시 분석했습니다. 그 결과, 전체 기능을 가로막는 제약을 기술적으로 세 가지로 정의할 수 있었습니다.
1. 성능 관점
- 스크롤 이벤트와 연동되는 대량 요소의 위치 처리 문제
- 수백 장에 달하는 이미지 리소스를 ATF에서 받아와야 하는 네트워크·디코딩 문제
2. 좌표 계산 관점
- 각기 다른 위치에서 출발한 이미지가 로고 형태를 이루도록 배치하는 좌표 설계 문제
이렇게 성능·네트워크·알고리즘 영역에 걸쳐 있는 각각의 문제들은 기능뿐만 아니라 좋은 유저 경험을 제공하기 위해 반드시 해결해야 하는 문제였습니다.
문제 해결하기
문제를 구성 요소 단위로 분해한 후 각 이슈의 근본적인 어려움을 파악하고, 해결이 불가능한 지점은 우회 전략을 통해 극복하는 방식으로 접근했습니다. 여기서는 세 가지 축(성능, 네트워크, 좌표 설계)을 중심으로 실제로 어떤 선택을 했는지 정리합니다.
1) DOM의 한계를 넘어서기
가장 먼저 부딪힌 문제는 스크롤에 연동되는 애니메이션을 어떻게 안정적으로 유지할 것인가였습니다.
좌표 이동이 불가능하다면 해당 기능은 의미가 없기 때문에, 우선적으로 꽤 많은 요소들이 프레임 단위로 발생하는 스크롤 이벤트에 최적화되어 이동할 수 있는 방법을 고민해야 했습니다. 요소의 좌표를 이동시키는 가장 간단한 방법은 CSS의 transform: translate(...)를 사용하는 것입니다. 레이아웃 재계산(reflow) 없이 GPU 합성(compositing) 단계에서 처리되기 때문에 단일 요소 혹은 소수의 요소를 이동할 때는 매우 효율적인 방식입니다.
하지만 이번 ATF처럼 프레임 단위로 수백 개의 transform 값을 갱신해야 하는 경우 부하가 발생할 수 있습니다.
- 스크롤 이벤트마다 JS 메인 스레드에서 수백 개 좌표 계산
- 각 요소의 style 업데이트
- 변경된 스타일을 반영하기 위한 레이어 트리(layer tree) 업데이트
즉, 실제로 성능을 떨어뜨리는 지점은 JS와 DOM이 함께 사용하는 CPU 구간이었습니다.

요소 수가 많아질수록 이 구간의 비용이 선형적으로 증가하고, 모바일 디바이스에서는 곧바로 FPS 저하와 프레임 드롭으로 이어졌습니다.
WebGL 기반 렌더링으로의 전환
일반 DOM을 이용한 방법으로는 element의 수가 많아질수록 부하가 발생할 수 있기 때문에 PixiJS 기반의 렌더링 방식을 선택했습니다. PixiJS는 내부적으로 Scene Graph 구조를 사용해 화면을 그리며, 각 노드의 좌표/스케일/알파값 등을 GPU 버퍼로 직접 전달합니다. 이 구조를 사용하면 렌더링 파이프라인을 아래처럼 단순화할 수 있습니다.

즉, DOM 기반에서 필요했던 3~4단계의 CPU 작업을 제거하고, "JS → GPU"라는 단일 경로로 줄였으며,
스크롤에 따라 DOM 노드의 스타일을 변경하는 것이 아닌, GPU가 참조하는 버퍼 상의 좌표 데이터를 수정하게 됩니다. 또한 ATF 영역 하위의 전체 스크롤 이벤트는 gsap 라이브러리를 사용해 통제했는데요, 이 부분과의 자연스러운 연동을 위해 gsap의 스크롤 진행을 기반으로 PixiJs 애니메이션이 동기화되도록 처리했습니다.
그 결과, 수백 개의 스프라이트 좌표를 프레임 단위로 갱신하더라도 메인 스레드 점유율을 상대적으로 낮게 유지할 수 있었고, 특히 모바일 환경에서 스크롤과 애니메이션이 동시에 일어나는 구간에서도 안정적인 FPS를 확보할 수 있었습니다.
Dom vs. PixiJS

- Dom (Chrome DevTools > Performance)

- PixiJS (Chrome DevTools > Performance)

2) 요청 이미지 줄이기
두 번째 문제는 ATF 진입 시점에 수백 장의 이미지를 어떻게 받아올 것인가였습니다. 이미지 하나의 용량을 경량화 하더라도 여전히 ATF에서 500장의 이미지를 개별적으로 요청하는 것은 현실적인 선택지가 아니었습니다.
HTTP/2 환경에서조차, 다음과 같은 비용이 누적됩니다.
- 수백 개의 요청/응답 헤더 처리
- 각 이미지 포맷(WebP/JPEG/PNG 등)에 대한 decompression 및 decode 비용
- 디코딩된 비트맵을 GPU 텍스처로 업로드하는 비용
특히 모바일 환경(셀룰러 네트워크, 낮은 CPU/GPU 스펙)에서는 이 비용들이 겹치면서 페이지 첫 진입 속도를 크게 저하시킬 수 있었습니다. 따라서 이미지를 어떻게 최적화해서 받아올 수 있을지에 대한 고민이 필요했습니다.
Texture Atlas 도입
이 문제를 해결하기 위해서 요청의 수를 어떻게 줄이지? 에 초점을 두고 설계를 진행했고, 개별 이미지를 그대로 요청하는 대신 Texture Atlas 구조로 묶어 전달하는 방식을 선택했습니다.
- 여러 개의 스프라이트(개별 이미지)를 하나의 큰 텍스처 이미지에 배치
- 각 스프라이트의 x, y, width, height를 JSON 메타데이터로 관리
예를 들어 Atlas.webp와 Atlas.json은 다음과 같은 형태를 가집니다.
- Atlas.webp

- Atlas.json
{
"image01": { "x": 10, "y": 20, "w": 64, "h": 64 },
"image02": { "x": 90, "y": 20, "w": 64, "h": 64 }
}
렌더링 시에는 이 정보를 기반으로 단일 텍스처에서 필요한 영역만 잘라 사용하는 방식으로 동작합니다. 약 500장의 이미지를 Atlas로 묶어 다음과 같이 요청해야 하는 자원의 수를 줄였습니다.
우선순위 이미지와 중복 사용 전략
성능을 향상시킬 수 있는 추가적인 방법을 고민하다가 단순히 Texture Atlas를 도입하는 것이 아닌, 두 가지 추가 전략을 적용했습니다.
- priority 이미지 분리
- 스크롤 이전 혹은 초기 구간에서 반드시 보여야 하는 이미지에 priority_ prefix를 부여
- Atlas 생성 스크립트에서 priority 이미지를 먼저 모아 별도 Atlas로 생성
- 이를 통해 초기 진입 시 필요한 이미지부터 빠르게 로드될 수 있도록 함

- 이미지 중복 사용
- 스크롤이 진행될수록 각 이미지의 화면 내 크기가 작아지고, 사용자가 개별 이미지를 식별하기 어려워지는 시점 이후에는 동일 이미지를 여러 위치에서 재사용하도록 로직을 구성했습니다.

- 실제로는 302개의 원본 이미지를 통해 총 500개 슬롯을 채웠으며, 일부 이미지는 최대 2회까지 중복 사용했습니다.
- 이로 인해 시각적인 품질 저하 없이도 원본 리소스 수를 추가로 줄일 수 있었고, 결과적으로 Atlas 수와 용량 측면 모두에서 더 나은 균형을 맞출 수 있었습니다.
최적화 이후 다운로드 성능 변화
- 개별(약 500장) 이미지 다운로드

- Texture Atlas 도입
- PC : 500장 → 11장 (97.8% ⬇, 총 로딩시간 약 34배 빠름)

- 우선순위 이미지와 중복 사용 전략 이후
- PC : 11 → 6 (45% ⬇, 총 로딩시간 약 1.5배 빠름)

결과적으로 Texture Atlas를 사용하면 개별 파일의 용량은 커질 수 있지만, 전체적인 관점에서 다음과 같은 이득을 얻었습니다.
- 네트워크 요청 수 감소 → 지연(latency) 절감
- 디코딩 횟수 감소 → CPU 부담 감소
- 텍스처 바인딩 전환 횟수 감소 → WebGL draw call/batch 효율 향상
- 모바일 환경에서 체감 성능 향상
3) 로고 형태를 만들기 위한 매칭 전략
마지막으로 남은 문제는 이미지를 어떻게 배치해야 오늘의집 로고 형태를 자연스럽게 만들 수 있을까였습니다.
서로 다른 위치로부터 오늘의집 로고의 형태로 수렴해야 하며, 로고의 외곽은 선명하면서도 내부는 균일하게 채우도록 배치하도록 고려해야 했습니다. 이를 위해 좌표 문제를 다시 두 부분으로 나눴습니다.
- 로고 안에서 이미지가 도착해야 하는 지점(Target Point)을 어떻게 만들 것인가
- 화면 바깥 혹은 주변에서 이미지가 출발하는 지점(Origin Point)과 어떻게 연결할 것인가
Target Point 생성 – 레이어 기반 분포 설계
최종 타겟 포인트는 하나의 방식으로만 생성하지 않고, 다음 네 가지 레이어를 합쳐 구성했습니다.
- Boundary Band – 로고 외곽을 따라가는 테두리 레이어
- Inner Band – 경계 바로 안쪽을 채우는 이중 테두리 레이어
- Interior – 로고 내부를 균일하게 채우는 영역
- Rim Supplement – 경계 근처의 빈 공간을 보강하는 영역
기본 아이디어는 간단합니다.
경계 영역은 SVG 로고 아이콘의 경계(path)를 추출한 뒤, svg-path-properties를 이용해 경로를 일정 간격으로 샘플링합니다. 그리고 이러한 샘플 지점들 주변에 방향/거리, 자연스러운 왜곡을 적용해 실제 포인트를 생성합니다.

또한 내부 영역은 Poisson Disk Sampling + Stratified Grid 샘플링을 혼합한 방식을 사용했습니다.
- Poisson 샘플링으로 최소 간격을 보장해 뭉침을 방지
- Stratified Grid 기반 샘플링으로 전체 영역을 빠르게 채움
- 이후 한 번 더 relaxation(라플라시안 스무딩 유사 방식)을 수행해 밀도가 지나치게 높은 구간을 완화
이 과정을 통해 최종적으로는 경계는 또렷하면서도 내부는 자연스럽게 채워진 로고 형태를 구현할 수 있었습니다.
Origin Point 생성 및 좌표 매칭 – Origin과 Target을 어떻게 연결할 것인가
Origin 포인트는 최초 노출 이미지의 고정 위치와 그 외 이미지들의 원형 분포로 나누어 간단히 처리했습니다. 이때 화면 밖에서 출발하는 각 이미지는 균등 분포된 각도와 랜덤 반경을 부여받아, 결과적으로 여러 방향에서 동시에 로고로 수렴해 들어오는 느낌을 줄 수 있도록 설정했습니다. 이후 두 좌표를 연결하는 과정에서는 기본적으로 헝가리안 알고리즘(Hungarian Algorithm)을 채택하여 모든 Origin–Target 쌍에 대해 최소 비용 완전 매칭을 보장하는 방식으로 구현했습니다.
기본 비용은 Origin–Target 간 유클리드 거리 제곱으로 두고, 로고 중심을 기준으로 출발 방향과 도착 방향의 각도 차이가 큰 경우 추가 패널티를 부여해 부자연스러운 이동을 줄였습니다. 또한 초기 고정 이미지(처음 노출되는 12개)에 대해서는 별도의 가중치를 적용해, 화면 초반에 중요한 이미지들이 의도한 위치와 방향으로 더 안정적으로 수렴하도록 처리했습니다. 최종적으로는 Atlas.json에 정의된 이미지 메타 정보와 위 과정을 통해 생성된 좌표를 기반으로 positions.json 파일을 사전 생성, 런타임에서는 이 정보를 그대로 읽어 사용하는 구조로 마무리했습니다.
// position.json 예시 [ { "origin": [777, 400], "target": [3, 7], "atlasImage": "atlas1.webp", "isPriority": true ... }, ... ]이렇게 함으로써 로고를 구성하는 각 이미지가 어느 위치에서 시작해 어느 위치로 이동해야 할지에 대한 복잡한 계산을 빌드 타임에 미리 완료하고, 실제 사용자 환경에서는 애니메이션에만 집중할 수 있도록 구조를 분리했습니다.
마치며
처음 이 기능을 마주했을 때는 ‘어디서부터 손대야 할지’ 명확하지 않았고, 성능과 구현 측면에서는 현실적으로 불가능해 보이는 영역도 존재했습니다. 그래서 각각의 이슈에 대해 먼저 왜 이 문제가 어려운지를 정의하고, 그 뒤에 어떤 대안을 적용해 우회하거나 극복할 수 있을지를 집중적으로 고민했습니다.
그 결과 전체 시스템을 바꿔야 하는 영역(WebGL 전환), 네트워크 구조를 재설계하는 영역(Texture Atlas 도입), 알고리즘적으로 접근한 영역(최적 매칭)으로 문제를 나누어 하나씩 해결할 수 있었습니다.
그리고 이번 경험을 통해 “어려워보이는 큰 문제도, 올바르게 분해하면 해결 가능한 문제의 집합이 된다.” 라는 인사이트를 얻을 수 있었습니다. 앞으로도 오늘의집은 높은 완성도의 사용자 경험을 위해 다양한 기술적 도전과 실험을 이어가겠습니다. 감사합니다.
