Skip to content

Interested in AI, automation, blockchain, web and apps

Seoul, KR--:-- GMT
Let’s Talk

Work/Web Development/EN

Pre-Opening Teaser Landing Site for an Exhibition

개막 전 전시 정보를 알리는 모바일 티저 사이트

개막 두 달 전, 사진 여섯 장으로 만든 몰입형 티저 랜딩

개막 두 달 전, 사진 여섯 장

아트 갤러리의 개막 전 티저 랜딩 페이지입니다. 관리자 페이지도, 회원도, 예약도 없습니다. 전시 정보는 한 번 써 넣으면 바뀌지 않습니다.

화면은 모바일 기준으로 잡았고, 지정된 전용 서체를 써야 했습니다(LGSmHaR.woff 옆에 LGSmHaR_0.eot을 같이 넣어 둔 이유입니다). 가진 재료는 갤러리 공간 렌더링 여섯 장과 영상 임베드, 전시 개요 텍스트가 전부였고, 쓰이지 않는 요소는 화면에서 감추는 방향으로 정리했습니다.

시스템 경계와 외부 의존
시스템 경계와 외부 의존

스크롤을 뺏지 않고 만든 몰입

풀페이지 스크롤 하이재킹(fullPage.js)을 걷어내고 평범한 문서 스크롤로 되돌렸습니다

두 번째 섹션에 영상 임베드와 접었다 펴는 건축가 약력이 들어오면서, 한 화면에 딱 맞춰 고정되는 섹션 모델로는 콘텐츠가 담기지 않았습니다. 스크롤을 뺏지 않아도 배경만 살아 있으면 몰입은 유지된다고 봤어요. 프로토타입이 이미 fullPage.js 위에 서 있었고 배경 로직이 onLeave / afterLoad 훅에 묶여 있어, 연출을 통째로 다시 써야 했습니다.

모자이크 연출을 DOM span 타일에서 <canvas> 한 장으로 옮겼습니다

타일 수는 화면 폭에 비례해 늘어납니다. 가로 15개 기준이면 섹션마다 200개가 넘고, 그걸 전부 animate() 로 흔들면 첫 화면부터 밀렸습니다. 빌드 없이 jQuery 하나만 얹기로 해서 애니메이션 라이브러리를 더 들일 수 없었습니다.

최종본은 번들러 없이 정적 파일 그대로 배포했습니다. parcel 은 프로토타입 레포에만 남겨 뒀어요

결과물이 index.html 한 장과 CSS 세 장, 이미지 몇 장입니다. 해시 파일명이 주는 이득보다 "고쳐서 올리면 끝"이 컸습니다. 전시 개요와 약력 문구가 마지막 주까지 확정되지 않았습니다.

배경은 동영상이 아니라 jpg 여섯 장과 CSS 애니메이션으로 만들었습니다

transform: scale(1.2) 줌인, background-position 이동, 12초 주기 opacity 크로스페이드를 -4s / -8s 로 어긋나게 겹치면 세 장짜리 루프가 끊김 없이 흐릅니다. 모바일 자동재생 정책도 수십 MB 비디오도 피할 수 있었어요. 대신 풀스크린 jpg 여섯 장이 첫 로드에 그대로 실립니다.

배포와 인프라 구성
배포와 인프라 구성

배경은 고정하고 문서는 흐르게

레포는 셋이지만 역할은 겹치지 않습니다. lg-signature 는 fullPage.js 와 parcel 로 연출을 실험한 스파이크, lg-signature-front 는 배경 엔진을 뺀 마크업·CSS 원본(.sec1 의 background 선언이 주석 처리된 채 남아 있습니다), lg 가 둘을 합친 최종본입니다.

최종본의 화면은 세 겹입니다. 맨 아래가 #bg, position: fixed 와 z-index: -1 로 뷰포트에 못 박힌 배경 그룹 셋이에요. 그룹 하나는 .layer 세 장을 12초 주기로 크로스페이드하고, 첫 그룹은 옆으로 흐르고 나머지는 줌인합니다. 그 위가 #wrap, 스크립트가 런타임에 만들어 붙이는 <canvas> 한 장입니다. 맨 위 #top 은 그냥 스크롤하는 문서고요.

세 겹을 잇는 것은 스크롤 핸들러 하나뿐입니다. requestAnimationFrame 으로 스로틀된 틱이 섹션 셋의 위치를 읽고, 경계를 넘은 순간에만 hide 클래스를 갈아 끼운 뒤 runMosaic() 을 다시 돌립니다. 배경과 문서는 서로를 모릅니다. 이 분리 덕분에 두 번째 섹션이 길어져도, 약력을 펴서 높이가 바뀌어도 배경 쪽은 손댈 일이 없었습니다.

핵심 데이터 모델
핵심 데이터 모델

타일 200개를 DOM에서 지우고 캔버스 한 장으로 옮기기

화면을 격자로 쪼개 반투명 <span> 을 그 수만큼 만들고, 셔플한 순서대로 animate({opacity: 0}) 을 걸어 하나씩 사라지게 합니다. 저도 처음엔 그렇게 했어요. 프로토타입은 가로 15개 기준으로 섹션마다 200개 남짓한 span 을 append() 하고 setInterval(..., 20) 으로 20ms 마다 한 장씩 지웠습니다. 타일마다 합성 레이어가 생겨 모바일에서 첫 프레임부터 밀렸고, afterLoad 훅이 섹션을 드나들 때마다 인터벌을 새로 걸면서 핸들을 버려 계속 쌓였습니다.

DOM 타일을 전부 버리고 뷰포트 크기 <canvas> 를 만들어 rgba(255,255,255,0.3) 으로 채운 뒤, 셔플한 격자 좌표를 clearRect() 로 한 칸씩 지웠습니다. 타일 개수는 폭에 따라 6 / 12 / 16 으로 나누고, 지우는 간격도 20 / (horiBoxAmount / 6) 으로 같이 나눴어요.

지울 대상이 노드가 아니라 픽셀이 되면서 리플로우도 합성 레이어 200개도 사라지고, 캔버스 한 장의 부분 갱신만 남습니다. 간격 보정이 특히 중요한데, 타일이 6개일 때와 16개일 때 전체 개수는 일곱 배 가까이 차이 납니다. 간격을 고정하면 데스크톱에서만 리빌이 일곱 배 길어져 느린 페이지가 되죠. 개수와 간격을 같은 비율로 움직여 화면 크기와 무관하게 걷히는 시간을 맞췄습니다.

임계값 대신 부호가 뒤집히는 순간으로 섹션 경계를 잡기

scroll 이벤트에서 scrollTop 을 읽어 if (scrollTop > 800) 처럼 섹션마다 숫자를 박아 비교하는 방법입니다. 화면 높이가 바뀌면 다시 재야 하고, 트랙패드에서 초당 수십 번 날아오는 이벤트마다 배경 교체가 재실행되지 않도록 따로 막아야 합니다.

섹션 셋의 getBoundingClientRect().top - bodyRect.top 을 매 틱 구해 직전 값과 곱하고, 부호가 뒤집혔는지(prevSectionNpos * sectionNpos <= 0)만 봤습니다. 뒤집힌 섹션이 있을 때만 그룹을 바꾸고 prevMatch 에 마지막 그룹 번호를 남겨 같은 그룹이면 건너뜁니다. 스크롤 이벤트는 ticking 플래그와 requestAnimationFrame 으로 한 프레임에 한 번만 흘려보냈고요.

부호 교차는 "지금 막 경계를 지났다"를 픽셀 상수 없이 표현합니다. 튜닝할 숫자가 없으니 화면 높이가 바뀌어도 코드는 그대로고, 한 프레임에 수백 px 을 건너뛰는 빠른 플릭에서도 경계를 놓치지 않습니다. prevMatch 가 재실행을 막아 주니 경계 근처에서 손가락을 떨어도 모자이크가 매번 다시 시작되지 않고요. rAF 스로틀은 배경 교체 비용을 화면 갱신 주기 이상으로는 내지 않겠다는 상한선이었습니다.

유저가 할 수 있는 일

페이지를 열면 흰 모자이크가 걷히며 갤러리가 드러난다
페이지를 열면 흰 모자이크가 걷히며 갤러리가 드러난다

아래 화살표를 눌러 공간소개 섹션으로 내려간다
아래 화살표를 눌러 공간소개 섹션으로 내려간다

섹션 경계를 넘는 순간에만 배경이 갈린다
섹션 경계를 넘는 순간에만 배경이 갈린다

건축가 소개를 같은 자리에서 펼쳤다 닫는다
건축가 소개를 같은 자리에서 펼쳤다 닫는다

공간소개 위쪽의 갤러리 영상을 재생한다
공간소개 위쪽의 갤러리 영상을 재생한다

예정 전시 소개를 끝까지 읽는다
예정 전시 소개를 끝까지 읽는다

ENTRANCE 를 눌러 영상 페이지로 넘어간다
ENTRANCE 를 눌러 영상 페이지로 넘어간다

좁은 화면에서 열어도 같은 속도로 모자이크가 걷힌다
좁은 화면에서 열어도 같은 속도로 모자이크가 걷힌다

프로토타입에서 휠로 섹션을 스냅 이동해 본다
프로토타입에서 휠로 섹션을 스냅 이동해 본다

없는 주소로 들어가면 안내 페이지가 뜬다
없는 주소로 들어가면 안내 페이지가 뜬다

1 / 1

한 번 잰 좌표와 멈추지 않는 타이머

솔직히 남겨 둔 것이 꽤 있습니다.

가장 마음에 걸리는 건 bodyRect 예요. document.body.getBoundingClientRect() 를 로드 시점에 한 번만 재서 계속 쓰는데, 배경 이미지가 늦게 떠 문서 높이가 바뀌거나 화면을 돌리면 기준점이 어긋납니다. 같은 맥락으로 onresize() 는 타일 개수만 갱신하고 캔버스의 width / height 는 그대로 둬서, 세로에서 가로로 돌리면 캔버스가 늘어난 채 남습니다. 다시 만든다면 둘을 한 함수로 묶어 리사이즈에 태우고, 위치 판정은 IntersectionObserver 로 옮기겠습니다. 브라우저가 이미 잘하는 일을 손으로 하고 있었으니까요.

runMosaic() 안의 setInterval 도 핸들을 잡지 않습니다. 프로토타입에서 인터벌이 쌓이는 문제를 겪고 넘어왔는데 축소된 형태로 같은 문제가 남았어요. 경계를 빠르게 왕복하면 겹칩니다.

배경 jpg 여섯 장은 프리로드도 지연 로드도 없이 첫 페인트에 전부 걸립니다. 세 번째 그룹은 한참 내려가야 필요한데도요. 마지막으로 prefers-reduced-motion 을 전혀 보지 않습니다. 화면 전체가 12초마다 줌인하고 크로스페이드하는 페이지에서 이건 취향이 아니라 빠뜨린 처리입니다.

Read next

내 집 시세와 아파트 실거래가 조회 웹뷰

Banksalad Housing Tab — 2020