Skip to content

Interested in AI, automation, blockchain, web and apps

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

Work/Web Development/EN

Boiler Replacement Application and Share Entry Campaign Site

보일러 교체 신청과 공유 응모를 받는 캠페인 사이트

데스크톱 시안 한 벌로 모바일까지 덮은 캠페인 사이트

모바일 시안 없는 데스크톱 한 벌

친환경 콘덴싱 의무화 정책을 알리면서, 노후 보일러 교체 신청과 SNS 공유 응모를 한 페이지에서 받는 캠페인 사이트입니다. 신청자는 이름과 연락처, 도로명 주소, 집 형태를 입력하고, 공유 참여자는 자기가 올린 글 주소를 남깁니다.

제약은 세 가지였습니다. 시안은 데스크톱 740px 폭 한 벌뿐인데 같은 시안이 모바일에서도 그대로 보여야 했고, IE 지원이 필요했으며, 배포 환경은 IP 하나를 쓰는 서버 한 대였습니다.

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

프레임워크 대신 rem 좌표계

프레임워크 없이 webpack 멀티 엔트리로 정적 HTML 세 장을 굽는 구성

화면의 95%가 디자인 PNG 슬라이스고, 상태라 부를 만한 건 폼 입력값뿐이었습니다. HtmlWebpackPlugin을 세 번 걸어 index.html, popup1.html, popup2.html을 각자의 청크와 함께 뽑았어요. 배포처가 정적 파일을 얹는 서버 한 대였고, 지원 하한이 IE9였습니다(@babel/preset-env의 targets.ie: 9).

루트 font-size를 100px로 두고 모든 길이를 rem으로 쓰는 좌표계

시안 px을 그대로 옮겨 적기 위해서입니다. rem(374)는 시안의 374px과 1:1이고, 뷰포트가 좁아지면 루트 값 하나로 페이지 전체 배율이 정해집니다. 모바일 시안이 없어서 데스크톱 시안을 균일하게 축소하는 것 말고는 선택지가 없었습니다.

jQuery, select2, 우편번호 스크립트는 CDN에서 받고 번들에서는 window.$로 참조

네이티브 select로는 시안의 파란 라운드 박스를 만들 수 없고, 주소는 우편번호 검색이 국내 사용자에게 가장 익숙합니다. 셋 다 전역을 전제로 해서 const $ = window.$로 받아 쓰고 jQuery를 번들에 다시 넣지 않았어요. 우편번호 스크립트가 전역 daum 객체 위에서만 동작해 모듈로 감쌀 수 없었습니다.

imagemin-webpack-plugin + pngquant, quality 하한을 95로

본문 글자가 전부 이미지 안에 있어서 압축 아티팩트가 곧 뭉갠 글자로 보입니다. 전송량은 줄이되 글자 경계는 지켜야 해서 범위를 95-100으로 좁혔습니다. 개발 중에는 disable: process.env.NODE_ENV !== 'production'으로 꺼서 리빌드 시간을 지켰습니다.

npm run upload 한 줄로 빌드 후 scp 배포

몇 주짜리 캠페인이라 CI를 세우는 것보다 검수 문구를 30초 안에 반영하는 게 중요했습니다. 전달받은 게 서버 IP와 root 비밀번호뿐이었어요. 롤백이 없고 비밀번호가 셸 환경변수로 흐르는 구조라, 캠페인 종료와 함께 폐기하는 것을 전제로 한 선택이었습니다.

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

HTML 세 장과 정적 서버 한 대

구조는 얇습니다. webpack이 엔트리 세 개를 굽고 CopyWebpackPlugin이 src/assets를 통째로 옮긴 뒤, build/를 서버 docroot에 그대로 붓습니다. 서버에서 도는 코드는 공유 응모를 받는 PHP 핸들러 하나뿐이에요.

화면 이동은 라우터가 아니라 window.open이 합니다. 신청 버튼은 popup1.html을 600×900 창으로, URL 남기기 버튼은 popup2.html을 600×700 창으로 엽니다. 데스크톱에서는 주소창 없는 팝업 창이고 모바일에서는 새 탭인데, 팝업 스타일시트에 @media (max-width: 600px)로 min-height: 100vh를 걸어 새 탭으로 열려도 전체 화면 폼처럼 보이게 했습니다.

나머지는 전부 남의 서버로 넘깁니다. 소개 영상은 YouTube iframe, 주소 검색은 우편번호 서비스, 공유는 각 SNS의 share 엔드포인트예요. 우리 서버가 지는 부담이 정적 파일 전송과 응모 저장뿐이라, 오픈 첫날 트래픽이 몰려도 걱정할 것이 없었습니다.

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

시안 px을 그대로 타이핑하고 배율만 바꾸기

시안이 데스크톱 한 벌뿐일 때 흔한 선택은 둘입니다. 브레이크포인트를 서너 개 잡고 구간마다 px을 다시 적거나, 요소마다 %와 vw를 손으로 환산하는 방법이죠. 앞은 구간 사이에서 시안과 어긋나고, 뒤는 이미지 스무 장의 좌표를 전부 다시 계산해야 해서 한 곳만 틀려도 겹침이 생깁니다.

루트 font-size를 100px로 못 박고, Sass 함수 rem($value)가 시안 px을 100으로 나눠 rem으로 바꾸게 했습니다. 그래서 width: rem(374)처럼 시안 수치를 그대로 적어요. 740px 미만에서는 html { font-size: calc(100vw / 740 * 100); } 한 줄로 루트를 뷰포트에 비례시키고 .wrapper만 width: 100%로 풀었습니다. 루트를 100px로 올리면 모든 글자가 100px이 되니 * { font-size: rem(10); }으로 기본값을 되돌려 두는 게 짝입니다.

페이지의 모든 길이가 한 단위계에 묶이면 배율은 루트 하나로 결정됩니다. 740px 이상에서는 시안과 1:1로 고정되고, 그 아래에서는 화면 폭을 740으로 나눈 비율만큼 페이지가 통째로 줄어들죠. 팝업은 기준만 600으로 바꿔 같은 규칙을 씁니다. 덕분에 모바일 시안 없이도 어긋나는 지점이 생기지 않았어요.

납작한 이미지 안의 글자를 링크로 만들기

동의 문구 이미지 안의 약관 글자만 눌리게 해달라는 요구를 받으면 보통 그 문장을 HTML 텍스트로 다시 조판하거나, 이미지를 세 조각으로 잘라 가운데만 <a>로 감쌉니다. 앞은 자간과 폰트 렌더링이 시안과 미세하게 달라져 검수에서 바로 걸리고, 뒤는 조각 경계가 확대·축소에서 실선으로 드러납니다.

이미지는 한 장 그대로 두고, 글자 자리에 투명한 div#marketing-link를 position: absolute로 얹었습니다. 좌표도 left: rem(225); width: rem(220); height: rem(14);처럼 rem으로 적고 z-index: 2로 이미지 위에 올린 뒤, 클릭 핸들러에서 약관 페이지를 새 창으로 엽니다. 메인의 공유 버튼 줄도 같은 방식이에요. 배경 띠 PNG 한 장 위에 아이콘 네 개를 :nth-child(n+2):nth-child(-n+5) 선택자로 55px 간격씩 절대 배치했습니다.

핫스팟이 이미지와 같은 rem 좌표계 위에 있어서, 뷰포트가 줄어 이미지가 60% 크기가 되면 핫스팟도 정확히 60%로 줄어 같은 글자를 덮습니다. px으로 적었다면 화면 폭마다 어긋났을 자리예요. 시안은 이미지 한 장으로 유지되고, 눌리는 영역만 코드가 알고 있는 구조가 됩니다.

유저가 할 수 있는 일

캠페인 페이지에 들어와 영상과 안내를 본다
캠페인 페이지에 들어와 영상과 안내를 본다

보일러 교체 신청 폼을 열어 채우고 제출한다
보일러 교체 신청 폼을 열어 채우고 제출한다

우편번호 검색으로 도로명 주소를 채운다
우편번호 검색으로 도로명 주소를 채운다

주택 타입과 평형을 고르고 기타면 직접 적는다
주택 타입과 평형을 고르고 기타면 직접 적는다

개인정보 이용에 동의하고 약관을 연다
개인정보 이용에 동의하고 약관을 연다

이벤트를 블로그, 카페, 페이스북, 카카오에 공유한다
이벤트를 블로그, 카페, 페이스북, 카카오에 공유한다

공유한 글 주소를 남겨 응모한다
공유한 글 주소를 남겨 응모한다

콘덴싱 보일러 안내와 보조금 정보를 확인한다
콘덴싱 보일러 안내와 보조금 정보를 확인한다

당첨자 후기를 눌러 공개 여부를 확인한다
당첨자 후기를 눌러 공개 여부를 확인한다

입력을 그만두고 팝업을 닫는다
입력을 그만두고 팝업을 닫는다

1 / 1

콘솔에서 멈춘 신청 폼 제출

솔직히 이 레포는 오픈 직전 상태로 멈춰 있습니다. 공유 응모 팝업은 popup2.php로 실제 POST를 보내지만, 교체 신청 팝업의 제출 버튼은 검증을 통과한 뒤 console.log만 찍고 끝나요(src/pages/popup1/popup1.js:89-96). 신청 데이터를 어디로 보낼지가 마지막까지 정해지지 않았던 자리입니다. 검증 로직도 두 팝업에 그대로 복사돼 있고, og:url에는 개발 중 쓰던 ngrok 주소가 남아 있어요. 공유 크롤러를 확인하려면 공개 URL이 필요해 붙였다가 도메인으로 바꾸지 못한 흔적입니다.

다시 만든다면 셋을 바꾸겠습니다. 폼 필드 정의를 배열 하나로 빼서 검증과 마크업을 거기서 만들고, 본문 텍스트를 이미지에서 꺼내겠습니다. 지금 페이지는 스크린 리더가 읽을 것이 alt="btn-1" 같은 문자열뿐이고, 문구 한 줄 고치려면 디자인 파일을 다시 받아야 하니까요. 그리고 sshpass로 root 비밀번호를 흘리는 배포 대신 배포 키와 심볼릭 링크 교체를 쓰겠습니다. 캠페인이 짧다는 이유로 넘어갔지만, 짧을수록 되돌릴 방법이 필요했어요.

Read next

은행·카드·투자를 한 화면에 모으는 자산관리 앱

Broccoli — 2019