배민선물하기에서는 생일과 같은 일반적인 상황뿐만 아니라 집들이나 이직 같은 다양한 축하 상황, 출산이나 입원 등 몸보신에 어울리는 상황까지 모두 아우를 수 있도록 1천 개 이상의 카드를 제공하고 있습니다. 이 중 사용자의 참여로 만들어진 카드도 52개 제공되었습니다. 오늘은 이 52개의 특별한 카드가 함께 서비스될 수 있었던 흥미로운 이야기를 공유하고자 합니다. 특히 이번 글에서는 PM, 서버, 프론트, 운영, 마케팅 등 다양한 직무의 구성원들이 한 팀으로 모여 하나의 캠페인을 어떻게 기획하고 실행했는지 그 과정을 세세하게 소개하고자 합니다. 유기적인 협업을 통해 이벤트를 구체화하는 과정, 고민했던 지점과 해결책, 그리고 최종적으로 도출된 결과물까지 다채로운 이야기를 시작하겠습니다.
2024 선물카드 제목 짓기 대회의 시작
지난 2022년부터 배민선물하기의 인지도를 높이기 위해 매년 선물 카드의 그림을 그리는 참여형 이벤트를 선보이며 많은 사랑을 받아왔습니다. 올해로 3회차에 접어든 이 참여형 이벤트를 한 단계 더 발전시키기 위해 약 4개월 전부터 깊은 고민을 시작했습니다.
왼쪽부터 2022년 배민생일카드 콘테스트, 2023년 배민응원카드 콘테스트, 2024년 선물카드 제목 짓기 대회
처음에는 기존 콘테스트와 유사하게 카드 그리기라는 큰 틀을 유지하면서 참여 난이도를 좀 더 낮추기 위해 동물이나 음식 같은 쉬운 주제로 변경하는 것만을 생각했습니다. 특히 ‘음식’이라는 소재로 카드 그리기 대회를 개최한다면 배달의민족이 가진 ‘음식’이라는 강점과 연결시켜 “음식 선물 = 배민선물하기”라는 브랜딩 메시지를 전달할 수 있을 것 같았습니다. 그러나 팀원들과 이벤트의 방향성을 논의하며 기획을 발전시키는 과정에서 이벤트를 하는 본질적인 질문과 마주하게 되었습니다.
“왜 그림 그리기 대회여야 할까?”
이전 카드 그리기 대회도 충분히 성공적이었지만 그보다 더 많은 사람들이 참여할 수 있도록 하기에는 ‘그림 그리기’라는 허들이 높다고 생각했습니다. 이 질문을 시작으로 이벤트를 진행해야 하는 목적 또한 보다 명확하게 할 수 있도록 고민을 이어갔습니다. 배민선물하기만의 차별점은 무엇인가? 그리고 이 차별점을 누구에게, 어떻게 알릴 것인가? 와 같이 배민선물하기만의 차별점과 강점을 깊이 있게 분석하기 시작했습니다.
저희가 발견한 배민선물하기의 가장 큰 강점 중 하나는 바로 ‘1,000가지가 넘는 다양한 선물카드가 있다.’는 점이었습니다. 또한, ‘나만의 카드라는 기능을 통해 특별한 진심을 전할 수 있는 서비스’라는 점에 주목했습니다. 따라서 이번 이벤트의 핵심 목표는 “배민선물하기에는 선물하기 좋은 다양하고 특별한 카드가 있다”라는 메시지를 널리 알리는 것으로 설정했습니다. 단순히 참여를 유도하는 이벤트가 아니라, 배민선물하기의 차별화된 가치를 고객들에게 전달하는 캠페인으로 확장하고자 했습니다.
떠오르는 아이디어: 선물카드 제목 학원
목표가 명확해지니 자연스럽게 실행 방향도 구체화되었습니다. “배민선물하기만의 차별점을 어떻게 재미있고 효과적으로 전달할 수 있을까?”라는 질문에 대한 답을 찾던 중, 영감받은 레퍼런스는 바로 “제목학원”이었습니다.
실제 배민선물하기에서 인기 있는 선물카드를 살펴보면, 카드의 메시지가 선물카드의 구매를 크게 좌우하고 있습니다. 이에 착안해 그림 그리기 대신 카드에 어울리는 문구를 적어 응모하는 ‘제목 짓기’ 형태의 대회를 열기로 결정했습니다. 참가자들이 부담 없이 탐색하고 창의적인 아이디어를 떠올릴 수 있도록 총 20개의 그림만 있는 카드들을 준비했습니다.
저희의 바람은 간단했습니다. 카드에 어울리는 제목이나 문구를 자유롭게 지어 응모할 수 있고, 누군가에게는 위로가 또 다른 누군가에게는 웃음과 감동을 줄 수 있는 선물카드가 탄생하기를 기대했습니다. 이를 위해 이벤트의 이름 또한 신중하게 고민했습니다. 무려 200개가 넘는 타이틀과 키 카피 후보 중 최종 후보까지 살아남은 타이틀은 “선물카드 카피 왕 선발대회”, “선물카드 제목 짓기 대회” 이 두 가지였습니다. 수많은 고민 끝에 최초 컨셉의 이미지를 자연스럽게 이어받으면서도, 직관적이고 쉬운 이름인 “선물카드 제목 짓기 대회”로 결정했습니다.
이벤트의 완결성을 높이는 기대효과와 가설
브랜딩 캠페인 진행 시 이벤트의 성공 여부를 단순히 참여자 수로만 판단하는 것은 아쉽다고 생각했습니다. 그래서 이번에는 초기 기획 단계부터 정성적인 기대효과뿐만 아니라, 이벤트가 배민과 배민선물하기에 어떤 긍정적인 영향을 주었는지 정량적인 결과까지도 측정할 수 있도록 꼼꼼하게 설계했습니다. 이벤트의 주요 지표는 1,2회차보다 높은 수의 응모 작품 수, 응모작 수로 설정했지만, 보다 다양한 관점의 결과를 도출하기 위해 참여 고객군의 행동 변화도 확인했습니다. 예를 들어 아래와 같은 고객 지표를 확인할 수 있도록 사전에 준비했습니다.
- 이벤트 참여 이후 배민의 신규 회원 가입자 수
- 이벤트 참여 이후 선물하기 서비스를 처음 경험한 고객 수
- 이벤트 참여 이후 선물하기 서비스로 복귀한 고객 수
- 응모 페이지 유입 경로
또한 참가자들이 자유롭게 의견을 남길 수 있는 “만족도 조사”도 사전 설계했습니다. 조사에는 이벤트에 대한 의견뿐만 아니라 배민선물하기 서비스 전반에 대한 피드백까지 포함했습니다. 그리고 만족도 조사 자체가 이벤트의 연장선처럼 느껴지도록 자연스러운 플로우를 구성해, 높은 응답률과 다양한 의견을 수집할 수 있었습니다. 나아가 수집된 의견은 단순한 고객의 일회성 피드백으로 끝나지 않고 이벤트와 서비스를 개선하기 위한 구체적인 액션 아이템으로 연결하여 앞으로의 선물하기 프로덕트와 이벤트의 완결성을 높이는 데에 활용될 예정입니다.
제약사항 속에서 피어나는 서비스 기획
이벤트의 상위 기획이 끝난 이후에는 PM과 개발자가 함께 모여 참여자들이 편리하고 즐겁게 이벤트를 참여할 수 있는 방법을 구현하기 위한 논의를 시작했습니다. 특히 이번 이벤트의 목표는 배민선물하기를 아는 사람도, 알지 못하는 사람도 모두 쉽게 참여할 수 있는 것이 중요했습니다. 카드 그리기 대회에서 제목 대회로 이벤트 방식을 크게 바꾸었는데 성과에서 차이를 만들지 못한다면 아쉬울 것 같았기 때문입니다.
‘서비스 기획은 뭘 하는 일인가요?’라는 질문을 받는다면 주니어 시절에는 ‘창의력을 발산하는 일이요!’라고 대답했을 것 같습니다. 그러나 연차가 조금 쌓여가는 이 시점에 다시 대답을 해본다면 ‘제약사항 하에서 기능을 만드는 일’ 이라고 조금 더 구체적으로 정의할 수 있을 것 같습니다.
이번 ‘카드 제목 짓기 대회’라는 제품을 만들기까지도 많은 제약사항이 있었습니다. 제품 개발에 필요한 인력, 출시까지의 일정, 주어진 예산이라는 한정된 자원 안에서 가장 효과적인 방법을 택해야 했습니다. 소중한 자원을 활용하는 만큼, 이 자원이 무엇을 위해서 쓰는 것인지 어떤 결과를 기대하고 쓰는 것인지 처음부터 정확히 설정하고 이 기준에 맞지 않다면 기능 개발을 과감히 생략할 필요가 있었습니다. 해야 하는 이유가 명확하지 않다면 자원을 사용해놓고도 그저 그런 결과밖에 얻지 못해서 자칫 낭비로 끝나버릴 수 있기 때문입니다.
이벤트 기획 처음에는 있으면 좋을 것 같은 수십 가지의 기능이 있었지만, 제약사항을 고려해 최종적으로 몇 가지로 핵심 기능을 추릴 수 있었습니다. 가장 고민을 많이 했던 기능 두 가지를 소개해 보겠습니다.
첫 번째. 로그인한 유저만 이벤트 참여 가능 VS 비로그인 유저도 이벤트 참여 가능
로그인 유저만 이벤트에 참여할 수 있도록 설계한다면 비교적 구현을 간단하게 끝낼 수 있었습니다. 이벤트 페이지로의 접근을 배민 앱에서만 열어두고, 로그인하지 않은 유저는 배민 로그인 화면으로 이동시키면 끝났기 때문입니다. 그러나 이번 이벤트는 인스타그램, 카카오톡, 유튜브 등 다양한 매체에서 유료 광고를 진행할 예정이었기 때문에 배민 가입이 되어있지 않거나 로그인을 하지 않은 분들도 이벤트를 인지하고 유입될 수 있는 기회가 많았습니다. ‘카드 제목 짓기 대회’를 외부 채널을 통해 인지한 분들이 배민 앱 설치가 안 되어있거나 로그인이 되지 않았다는 이유로 아쉽게 이탈되기를 원치 않았습니다.
때문에 비로그인 유저도 참여할 수 있도록 퍼블릭 사이트를 구축하는 것을 필수 기능으로 잡았습니다. 비로그인 유저에게 작품 접수 권한을 열어주되 추후 경품 선정 등을 위해 참여자 개개인을 식별하기 위해 인증번호(OTP)를 발급하는 기능이 필요했습니다. 이름이나 닉네임 같은 경우는 중복이 발생할 수 있었기 때문에 휴대폰 번호를 key 값으로 삼고 해당 휴대폰 번호로 인증번호를 SMS, 카카오톡 어떤 방식으로 발급할 것인지에 관한 정책과 인증번호의 유효시간을 얼마나 유지할 것인지 세세한 정책이 필요했습니다. 하루에 너무 많은 인증번호를 발급하는 어뷰징을 막아야 하면서도, 실제로 이벤트에 많이 참여하고 싶어서 인증번호를 많이 발급하려고 하는 경우에 불편을 주지 않기 위한 정책을 잡았습니다.
이번 이벤트의 목적에 지난 이벤트들보다 참여 허들을 낮추어 더 많은 사람이 참여하게 한다는 분명한 목표가 있었기 때문에 로그인을 하지 않은 사람도 참여할 수 있도록 하는 기능을 과감히 필수 기능으로서 검토하였습니다.
(좌) 인증번호 발급 화면, (우) 카카오톡 인증번호 도착 알림톡
숫자가 아닌 문자로 된 인증번호 발급을 고민했던 시절
두번째. 모바일에서만 VS 모바일+PC 둘다?
PC 브라우저에서도 이벤트 참여가 가능하도록 구현하려면, 모바일 화면/기능 구현 작업에 추가적으로 브라우저용 화면 대응을 위한 반응형 작업이 추가로 필요하여 프론트 개발, 디자인 리소스를 더 많이 사용해야만 했습니다. 때문에 모바일에서만 이벤트를 진행할지, 반응형을 구현해서 여러 환경에서 참여할 수 있게 구현할지 의사결정이 필요했습니다.
배민선물하기의 경우에는 대량 발송을 위한 고객분들을 위해 PC 사이트를 제공하고 있습니다. 때문에 카드 제목 대회 이벤트를 PC를 통해 인지하시는 분들도 있을 것이라 생각하였습니다. 외에도 대학생이나 직장인의 사용이 많은 배민선물하기 서비스 특성을 고려하여 Chrome, Safari와 같은 다양한 브라우저에서도 접근이 가능하도록 열어두는 것이 편리하고 심리스한 사용자 경험(Seamless UX)이 될 것으로 판단하여 크로스 브라우징(Cross-Browsing) 작업 및 반응형 작업을 진행하는 것으로 결정했습니다.
다만 처음부터 PC로 유입되는 사용자들이 모바일보다 더 많을 것이라 가정한 것은 아닙니다. 만약 PC로 유입되는 참여자 수가 너무나 적다면 추후 이벤트 진행 시에는 반응형 작업을 진행할지 말지 결정할 수 있는 기준 수치를 마련해 보자는 테스트 목적이 강했습니다. 이처럼 정량적인 성과가 아주 크지 않을 것으로 예상되더라도 리소스를 쓰는 것으로 결정하는 기능도 있습니다. 물론 이런 경우는 다른 건들 보다 상위 결정자와 진행 여부에 대해 좀 더 긴밀한 논의 후 진행이 필요합니다.
이외에도 카드에 제목을 지을 때 실시간으로 문구를 반영하게 할 것인지, 폰트의 종류나 크기를 다양하게 제공할 것인지, 응모 이력을 재조회 및 공유할 수 있게 할 것인지, 한 사람당 작품 접수 수를 제한할지 등을 결정해야 했습니다. 있으면 좋은 기능들이지만 꼭 필요하고 중요한 것만을 선택하는 과정을 거쳤습니다. 이 중에는 실현이 된 것도 있고, 제한된 자원을 고려하여 의도적으로 진행하지 않은 것들도 있습니다. 이처럼 서비스를 기획하는 일에는 작업자들의 효율성과 사용자들의 만족도 모두를 최적의 상태로 만드는 게 중요하다는 것을 깊이 깨달을 수 있는 프로젝트였습니다.
반응형 웹, 그거 어떻게 하는 건데
이번 선물카드 제목 짓기 대회를 진행하면서 프론트엔드에서 신경 썼던 부분은 바로 반응형 웹을 제작하는 것이었습니다. 원래 선물하기 서비스는 모바일 웹뷰와 PC 웹을 별도 프로젝트로 관리하고 있기 때문에 동일한 기능이더라도 양쪽에 모두 구현해야 했습니다. 별도로 관리하는 이유는 모바일이 출시되고 나서 한참 뒤에 PC가 출시되기도 했고, 회원이나 결제 등을 각 디바이스 환경에 맞게 구현해야 했기 때문입니다.
하지만 이번 이벤트 페이지는 작업 일정이 다소 짧기도 했고, 회원이나 결제 등의 구현이 필요 없었습니다. 그래서 PC 페이지로 한 번만 구현하고, 해당 페이지들을 모바일과 PC 모두에서 사용하기로 했습니다. 그러다 보니 반응형으로 웹을 구현하는 것이 중요한 화두였습니다. 문제는 팀원 전체가 반응형 웹 구현 경험이 없었기 때문에 기준이나 노하우가 다소 부족하다는 것이었습니다. 검색해 보니 반응형 웹을 구현하는 방법은 너무 다양했고, 그에 반해 눈에 보이는 명확한 기준을 가지고 반응형 웹을 구현한 예시를 찾기는 어려웠습니다. 그래서 반응형 웹 구현을 진행하면서 여러모로 혼란스러웠던 기억이 있네요.
처음 프로젝트를 검토할 때 단순히 반응형 웹을 완성도 있게 구현해 내는 것 외에 한 가지 목표를 더 세웠는데, 그건 앞으로 팀원들이 반응형 웹 작업을 할 일이 있을 때 참고할 만한 가이드라인을 간단하게나마 만들어 놓는 것이었습니다. 개인적으로 개발 검토를 진행할 때 마땅한 기준이 없어서 조금 막막했거든요. 또, 앞서 언급되었듯 선물하기에서는 매년 카드 콘테스트를 콘셉트로 이벤트를 진행하고 있기 때문에 앞으로도 종종 이벤트 페이지 제작이 필요할 것으로 예상되는데, 그럴 때 반응형 웹에 대한 요구사항이 있을 수도 있겠다고 생각했습니다. 그래서 이후에 다른 팀원들이 반응형 웹을 구현할 때는 조금이라도 덜 막막했으면 좋겠다는 생각이 들었습니다.
반응형 UI 분기
사실 처음에는 이 ‘반응형 UI’라는 요구사항이 다소 애매하게 다가왔습니다. 보통 반응형 UI는 미디어 쿼리로 구현한다고 알고 있었는데, 기획서와 디자인 시안은 PC/모바일로 구분이 되어있었습니다. 미디어 쿼리를 사용하면 화면 너비에 따라 분기를 처리한다는 말인데, PC/모바일 구분은 사실 화면 너비보다는 navigator.userAgent를 사용하는 게 더 확실하게 구분하는 방법 같았습니다. 어떤 부분을 미디어 쿼리로 처리하고 어떤 부분을 userAgent로 처리할지에 대한 기준이 모호했고, 상황에 따라 간편한 방법을 선택했습니다. 애초에 어떤 부분을 미리 계획하고 고려해야 할지도 몰라서 사전에 검토를 하지 못했고, 그 상태로 반응형 UI 구현을 끝냈습니다.
그렇게 겉보기에만 완벽한 반응형 UI 코드를 팀원들께 리뷰하고 나니, 팀원들의 반응이 당연히 심상치 않았습니다. 가장 강하게 들어온 피드백은 코드의 직관성, 즉 가독성이 떨어진다는 것이었습니다. 일정에 맞춰 빠르게 제작하려다 보니 그때그때 편한 방법을 선택했는데, 그러다 보니 미디어 쿼리(CSS), matchMedia API, userAgent가 골고루 섞이게 되었던 것이었습니다. 사실 저는 코드가 익숙하기도 했고, 반응형 로직을 hook으로 분리했기 때문에 크게 문제가 없을 거라고 생각했습니다. 하지만 코드를 처음 접하는 다른 팀원들의 입장에서는 hook 내부에서도 구현 방법이 제각각이어서 직관성이 떨어지고, hook의 반환값들이 컴포넌트 여기저기에 섞여 들어가면서 다른 코드에도 영향을 끼치게 됐던 거죠.
지속적인 논의 끝에 저희는 반응형 UI 분기는 CSS 미디어 쿼리만으로 처리하고, 환경에 따른 기능 분기(PC/모바일)는 userAgent로 명확하게 나누어 처리하는 것이 좋겠다고 결론 내렸습니다. 이러한 결론을 내린 데에는 Next.js의 서버 사이드 렌더링 환경도 영향이 컸습니다. 서버 사이드 렌더링 환경에서 JS로 PC/모바일 분기를 처리하려면 getServerSideProps 같은 함수를 통해 HTTP 헤더의 user-agent를 참조하고 props를 내려주거나, 컴포넌트 내부에서 일일이 navigator 객체가 존재하는지 아닌지에 대한 분기를 처리해야 합니다. 반면에 CSS는 브라우저에 도달한 이후에 화면에 반영되기 때문에 서버 사이드 렌더링에 영향을 미치지 않습니다. 그래서 JS를 사용할 때처럼 별도의 분기를 처리할 필요가 없고, 결국 코드의 복잡도가 낮아지는 결과로 이어지게 되는 것입니다.
큰 방향에서의 가이드라인을 정했으니, 이제 세부적으로 어떻게 구현을 수정하면 좋을지에 관해 논의해 볼 차례였습니다. CSS로만 반응형 UI를 구현하기로 결정하면서 맞닥뜨린 가장 큰 문제는, 디자인상 어쩔 수 없이 모바일에서만 혹은 PC에서만 존재하는 컴포넌트들이 있다는 것이었습니다. 해당 컴포넌트가 모바일용인지 PC용인지 알기 위해서는 컴포넌트를 파고 들어가서 CSS 를 들여다봐야 했기에 코드 파악이 다소 불편했습니다. 이를 개선하기 위해서 특정 컴포넌트를 모바일에서, 혹은 PC에서만 노출되게 하는 별도의 래핑 컴포넌트를 만들어 감싸는 방식을 선택했습니다.
const MobileVisibleOnly = styled.div` display: none; ${MediaQueries.MOBILE} { display: flex; } `; const DesktopVisibleOnly = styled.div` display: flex; ${MediaQueries.MOBILE} { display: none; } `; const Responsive = { MobileVisibleOnly, DesktopVisibleOnly }; export default Responsive;이런 식으로 컴포넌트를 정의해놓으면, 사용하는 곳에서는 다음과 같은 방식으로 사용할 수 있습니다.
<NavigationBar> <Responsive.MobileVisibleOnly> <Wrapper1 onClick={handler}> {isLeftImage ? <Image src="/image_for_mobile.png" /> : null} </Wrapper1> </Responsive.MobileVisibleOnly> <Responsive.DesktopVisibleOnly> <Wrapper2 onClick={handler}> <Image src="/image_for_PC.png" /> <Title>PC용 네비게이션 바</Title> </Wrapper2> </Responsive.DesktopVisibleOnly> <Icon src="/share_icon.png" /> </NavigationBar>사실 래핑 컴포넌트를 작성하는 순간 CSS 미디어 쿼리만으로 분기를 처리한다는 기준을 살짝 침범하는 느낌이 없지 않아 있지만, 그래도 래핑 컴포넌트를 사용하는 것이 읽기 편하다는 의견이 많았습니다. 물론 이 방법도 JSX 코드량이 다소 많아진다는 단점이 있기는 합니다. 하지만 아직까지는 단점보다는 장점이 더 와닿는다고 생각하고 있습니다.
한 가지 첨언하자면, 이런 방식은 화면 사이즈에 따라 다른 영상을 보여줘야 하는 이번 이벤트 페이지에 아주 적합했습니다. 이번 이벤트 페이지의 초기 화면에는 꽤 많은 양의 영상이 삽입되었는데, 이 영상들의 파일 사이즈를 줄여 로딩 속도를 빠르게 만들기 위해 .gif 대신 .mp4 포맷을 사용했습니다.
.mp4 포맷을 사용하면서 맞닥뜨린 한 가지 문제는, img 태그와 다르게 video 태그에는 화면 사이즈에 따라 반응형으로 영상 소스를 교체해 줄 수 있는 기능이 없다는 것이었습니다. 따라서 자바스크립트를 활용하여 영상 소스를 교체하는 로직을 작성해 줘야 하는데, 위의 Responsive 컴포넌트를 사용하게 되면서 그럴 필요도 자연스럽게 사라지게 되었습니다. 해당 컴포넌트를 사용하면 자연스럽게 모바일과 PC를 위한 영상을 초기에 모두 불러오고, 화면 사이즈에 따라 다르게 보이도록 구현할 수 있었기 때문입니다. 대신 초기에 너무 많은 영상을 불러와 로딩이 느려지지 않게 하기 위해, 첫 화면에 보이지 않는 비디오 및 이미지들에는 Lazy Loading을 적용했습니다.
디바이스별 기능 분기
모바일과 PC의 기능에 분기가 들어가는 예시를 들어보면, 모바일의 경우 이벤트 페이지를 다른 사람들에게 공유하는 로직이 URL Scheme을 통해 동작하는 반면 PC의 경우에는 단순히 클립보드에 복사하는 방식으로 동작하는 것이 대표적입니다.
처음에는 이런 동작의 차이 또한 화면 너비로 분기를 줘서 구현했는데요, 아무래도 디자인 분기가 화면 너비에 의존하기 때문에 기능도 그에 맞춰서 달라져야 하지 않을까 하는 생각이었습니다. 하지만 QA 과정에서 로직이 생각했던 대로 동작하지 않는다는 것을 알게 됐습니다. 미디어 쿼리를 screen의 width를 기준으로 분기했는데, 이는 정확하게는 디바이스 화면 너비가 아니라 뷰포트의 너비를 기준으로 하기 때문입니다. 아까의 공유 로직을 다시 예로 들어보면, PC에서 브라우저 너비를 줄여서 사용하면 뷰포트 너비가 줄어들기 때문에 미디어 쿼리의 screen width 또한 줄어들게 됩니다. 그래서 모바일에서 사용하는 URL Scheme으로 공유 로직이 동작하여 아무 반응이 없게 됩니다.
이러한 문제는 navigator.userAgent를 사용하여 개선할 수 있었습니다. userAgent를 사용하면 현재 사용자의 브라우저 종류에 따라 문자열이 달라지기 때문에 모바일 브라우저 환경인지 아닌지를 쉽게 구분할 수 있습니다.
const isMobile = /Mobile|Samsung|SM-|iPhone|Moto|Motorola|Xiaomi|Mi|Redmi|MIX|POCO/i.test( navigator.userAgent );이렇게 하면 브라우저 너비를 줄이더라도 모바일 브라우저 환경이 아니므로, 모바일 로직이 아닌 PC 로직이 동작하게 되는 것이죠. 해당 로직은 간단한 유틸 함수로 분리하여, 공유 로직뿐만 아니라 기기 별 분기가 필요한 곳곳에 사용되었습니다.
소통의 중요성
지금까지 전부 코드에 대해서만 말씀을 드렸는데요, 사실 반응형 웹 구현에서 가장 중요하다고 생각하는 것은 따로 있습니다. 개발자들끼리 코드에 관한 기준을 맞추는 것도 당연히 좋지만, 반응형 웹 구현은 무엇보다 기획/디자인적인 요소의 영향을 많이 받는다고 생각합니다. 그래서 기획자, 디자이너와 많이, 자주 소통하는 것이 정말 중요합니다. 미디어 쿼리 breakpoints, 화면 너비에 따른 텍스트 줄바꿈 처리 등 개발자가 혼자서 처리하면 골치 아픈 것들이 소통을 통해 금방 해결되는 경우가 많았습니다. 위에서 언급했던 PC/모바일의 UI가 다른 부분도 디자이너와 상의하여 최대한 디자인을 유사하게 맞추는 과정을 통해 일부 해결할 수 있었습니다.
초반에 몇 번 싱크업 미팅을 하더라도, 개발자가 보는 요구사항과 PM/디자이너가 보는 요구사항은 다를 수밖에 없습니다. 사용하는 용어도 다르고 경험도 다르기 때문입니다. 그 차이를 메꾸기 위해 지속적으로 확인하고 싱크업을 해야 합니다. 당장 저만 하더라도, 처음에 PC/모바일이 어떤 기준으로 구분되는 건지 모호했을 때 싱크업을 통해 내용을 확인하고 어떤 기준으로 구분할지를 재차 확인했더라면 먼 길을 돌아가지 않아도 됐을지도 모릅니다.
그래서 사실 가이드라인을 만들었다고 했지만, 개발에 대한 내용은 ‘미디어 쿼리만 사용하라’던가 ‘어떤 상황에 어떤 걸 사용하라’던가 하는 정도의 간단한 내용이 전부입니다. 그 대신, 프로젝트 초기에 디자이너와 어떤 부분들에 관해 미리 논의하면 좋을지에 대한 내용이 많습니다. 확실히 처음에 미리 정해두고 가면 편했겠다 싶은 부분이 많았거든요. 제가 경험한 시행착오를 다른 분들은 느끼지 않았으면 하는 생각에서 가이드라인을 작성했기 때문에, 자연스럽게 관련 내용이 가장 많은 분량을 차지하게 된 것 같습니다.
개인적으로 반응형 웹 UI는 어느 정도 시간과 꼼꼼함만 있다면, 누구나 제법 퀄리티 있는 결과물을 만들어낼 수 있다고 생각합니다. 중요한 건 정해진 시간 내에 얼마나 시간 효율적으로 구현하는가, 얼마나 코드를 가독성 있게 작성하여 유지 보수를 수월하게 만드는가 하는 것이라고 생각합니다. 하지만 이 부분에 대해서는 마땅히 정해진 게 없다 보니 아마 저처럼 처음 반응형 UI를 구현하시는 많은 분들이 헤맬 수 있을 것 같습니다. 그래서 이번 이벤트를 진행하면서 개인적으로 느꼈던 중요한 내용들에 대해 가이드라인을 작성하고 기술 블로그 글도 써봤는데, 여러분들이 반응형 웹을 구현하시는 데에 조금이나마 도움이 되었으면 좋겠습니다.
유연하고 확장 가능한 이벤트 구조 만들기
앞서 배민선물하기는 2022년부터 매년 카드 그리기 대회 콘셉트의 이벤트를 진행했고, 그림 그리기에서 글쓰기로 참여 난이도를 낮췄다고 한 이야기 기억나시나요? 저희 선물하기의 개발자들은 이렇게 변화하는 이벤트 마케팅과 기획 요구사항들에 맞춰 개발을 진행해 나가고 있습니다. 변화 무쌍한 요구사항에 맞게 중점적으로 고민했던 부분에 대해 이야기를 풀어보겠습니다.
전시 도메인 속 이벤트, 모듈 분리로 부담 줄이기
선물하기는 전시, 구매, 상품권 등의 도메인들을 가지고 있습니다. 이번 배민선물카드 제목 짓기 대회 구현에 앞서 이러한 도메인들 중 어느 도메인에 개발을 진행할지 논의를 진행했습니다. 그 결과, 새로운 이벤트 페이지가 생겨 시각적인 서비스를 한다는 점과 선물카드를 주제로 하는 점을 고려했을 때 전시 도메인에 구현하는 것이 적합하다는 결론이 나왔습니다.
그런데 이렇게 단기 이벤트로 사용될 코드를 전시 도메인에 곧바로 구현할 경우 우려되는 점이 있었습니다. 위에서 언급한 것처럼 선물하기에서는 해마다 이벤트를 새롭게 구상하여 오픈하기 때문에 이전의 이벤트 코드는 더 이상 사용하지 않는 코드가 될 가능성이 높다는 것이었습니다. 작년의 그림 그리기 콘테스트는 잠시 묻어두고, 제목 짓기 대회로 다시 태어난 것처럼 말입니다.
전시 도메인에는 선물하기 지면의 카드나 상품 등 전시되는 프로덕트를 위한 데이터들이 있고 이를 처리하는 로직들도 포함하고 있습니다.
반면 이번과 같은 이벤트 도메인은 주로 응모작, 응모자 정보, 응모 일시 등의 기존 선물하기 전시 도메인과는 크게 연관성이 없는 데이터들을 가지고 있으며 로직 또한 이벤트에 맞춘 제한된 요구사항을 구현한 것들이 많이 포함되어 있습니다.
두 도메인이 다루는 데이터와 로직이 달라, 이벤트를 전시 도메인에 구현하면 불필요한 의존성이 생길 수 있었습니다. 이후 이벤트 코드가 사용되지 않고, 새로운 이벤트 코드를 구현하게 된다면 레거시 코드는 점점 쌓이고 레거시 코드의 의존성 제거를 위한 검토 및 구현, 테스트 절차의 리소스가 점차 커지게 됩니다.
그래서 이벤트 코드 구현에 대한 두 가지 요구사항을 도출했습니다.
- 쓰지 않게 된 이벤트 코드의 Fade Out이 쉽도록 합니다.
- 이벤트 도메인의 분리가 쉽도록 합니다. (나중에 이벤트가 정형화된다면 이벤트 도메인을 위한 서버 자체를 분리하는 것을 염두에 둔 것입니다.)
위 사항들을 충족하기 위해서는 이벤트 코드를 전시 도메인에 구현하되, 다른 모듈로 분리해야 한다고 생각했습니다. 그래야 제거도, 분리도 쉬울 것이라고 생각했기 때문입니다. 전시 도메인은 domain 모듈을 의존하는 external-api 모듈이 있고 그것이 외부 트래픽을 받는 서버로 올라갑니다. 그리고 이 서버에 이벤트 모듈을 함께 배포하는 것을 고려했습니다. 이벤트는 자주 열리지 않고 언제든 사라질 수 있기에 이벤트 모듈만을 위한 별도의 서버 인프라를 새로 구축하는 것은 리소스를 효율적으로 사용하지 않는 것이라고 느껴져 서버를 함께 사용하기로 하였습니다.
따라서 이벤트 도메인과 이벤트 애플리케이션 코드를 contest라는 모듈에 함께 두고 external-api 모듈에 contest 모듈 의존성을 추가했습니다. 모든 이벤트 관련 코드는 contest 모듈 안에 있고 external-api 모듈은 그에 대한 의존성만 가지고 있습니다. 그러면 external-api가 서버에 배포되면서 이벤트는 전시와 서버 리소스를 공유하되, 의존성은 최소화했습니다. 때문에 이벤트가 지속되지 않을 경우 contest 모듈 자체를 제거한다면 간단하게 Fade Out 할 수 있습니다. 반대로 이벤트가 지속되고 정형화된다면 이벤트 모듈을 이벤트 서버로 따로 분리하기 위한 마이그레이션도 쉽게 해낼 수 있게 되었습니다.
누구나 쉽게 참여 가능한 OTP 인증 도입
비회원도 참여가 가능한 만큼 최대한 많은 참여를 이끌어 낼 수 있도록 복잡한 인증 절차를 없애고 빠르게 인증할 수 있는 방식을 고려했습니다. 인증 방식을 OTP로 직접 구현할지 아니면 휴대폰 인증 시에 누구나 한 번 쯤 접해본 PASS 인증 Open API를 사용할지 고민하다 직접 구현하기로 결정하였습니다. 사용자의 본명, 생일, 성별 등의 상세한 정보들이 필요 없을 뿐만 아니라 본인의 다른 휴대폰 번호, 즉 CI(Connecting Information)를 수집해 중복 제출을 막는 등의 정교한 유효성 검사를 할 필요가 없었기 때문입니다. 직접 구현으로는 휴대폰 번호만 가지고도 본인인증을 할 수 있도록 구현할 수 있어 인증 절차에 대한 허들이 낮아 부담스럽지 않게 참여를 유도할 수 있었습니다. 그로 인해 많은 사람들이 간편하게 접수할 수 있지 않았나 싶습니다.
OTP 기반 인증을 구현하는 방식에서 Redis를 저장소로 많은 사람들이 사용하고 추천하고 있습니다. OTP를 발급하고 검증하는 과정에 초점을 맞추어 생각해 보면 Redis를 사용한 이유는 다음과 같습니다.
- OTP 값에 TTL(Time To Live) 설정을 통해 일정 시간 내에서만 사용 가능하도록 OTP 만료에 대한 관리가 쉽습니다.
- OTP 발급과 검증 시엔 읽고 쓰기가 많이 이루어지는데 Redis의 높은 처리 성능 덕분에 효율적으로 처리가 가능합니다.
- 원자적 연산(예: INCR, SADD, DECR 등)을 제공하기 때문에 데이터 무결성 면에서 유리합니다. 따라서 여러 클라이언트가 동시에 접근해도 동일한 OTP가 여러 번 사용되거나 중복 발급되는 일은 발생하지 않습니다.
이러한 Redis의 특장점때문에 OTP 인증 구현에 적합한 저장소라고 생각했습니다.
OTP 발급/검증 흐름도
위 흐름도를 간단히 정리하자면, 먼저 OTP 발급 전 발급 가능 여부와 실패 횟수를 확인한 뒤 OTP를 생성합니다. 이후 검증 단계에서는 만료나 불일치 여부를 확인하고, 모든 조건이 충족되면 세션을 생성하는 방식으로 동작합니다.
휴대폰번호별 OTP 저장 키 : cardNameContest:otp:$phoneNumber OTP 발급 횟수 제한 키 : cardNameContest:otp:$phoneNumber:count:generate:$date 인증 실패 횟수 관리 키 : cardNameContest:otp:$phoneNumber:count:failOTP 발급/검증시 생성되는 key 형태들
Redis의 원자적 명령어(GETDEL, INCR 등)를 활용하여 OTP 관리 로직을 구현하였습니다. 예를 들어, 사용자 A(01012345678)에게 OTP를 발급할 때 GETDEL을 사용해 기존에 할당된 OTP가 있으면 즉시 삭제한 뒤 새 OTP를 설정함으로써 번호별로 고유한 OTP를 발급하였습니다. 또한 INCR 명령어로 발급 횟수를 원자적으로 증가시켜 일일 발급 한도를 관리하고, 인증 실패 시에도 동일한 명령어를 활용해 실패 횟수를 정확히 기록하여 과도한 시도를 막았습니다. 이러한 접근을 통해 여러 프로세스가 동시에 접근하더라도 데이터의 일관성과 신뢰성을 유지할 수 있었습니다.
이벤트 PK 설계 방법 : 공유는 쉽고, 예측은 어렵게
이번 이벤트에서 고려할 부분 중 또 하나는 응모작의 key 설계였습니다. 응모작에 대한 데이터는 MySQL에 테이블 형식으로 저장할 예정이었고 Primary Key(PK)의 형식 결정이 필요했습니다.
기획 및 마케팅적 요구사항 중 몇 가지를 이야기해 보자면, 응모작은 다른 사람에게 공유가 가능하고, 공유 받은 사용자가 응모작을 확인할 수 있도록 해야 했습니다. 또한 1인당 응모 작품 개수에 제한이 없으며 본인의 응모작을 제출 순서대로 조회할 수 있어야 했습니다. 여기서 주의해야 할 점이 개수에 한도가 없는 응모작들을 서버에서 클라이언트에 한 번에 응답해 준다면 한 유저의 응모 수가 아주 많을 경우 서비스에 부하가 오거나, timeout 에러가 발생할 위험이 높습니다. 때문에 클라이언트와의 데이터 교환 방식으로는 Keyset Pagination 방식을 선택했습니다. 페이징 방식에서 응모작의 key를 데이터 조회의 기준점으로 삼으려 하였습니다.
위에서 말씀드린 요구사항들을 지키려면 결론적으로 두 가지를 고려해야 합니다.
- 공유 받지 않은 응모작에 접근이 불가해야합니다. 예를 들어 URL 주소창에서 응모작 id를 유추하여 입력하는 식으로 다른 응모작을 조회할 수 없어야 합니다.
- 응모 이력 조회 시 페이징에 이용할 key는 제출 시간에 따른 순서를 가지고 있어야 합니다.
첫 번째 조건을 충족하기 위해서 응모작에 대해서 PK 사용 시 Auto Increment Integer 대신 UUID를 사용했습니다. 대부분의 사람들이 생각하고 사용하는 UUID의 버전은 v4이고 랜덤 데이터로 생성된다는 특징이 있습니다. 이러한 UUID는 고유하며 경우의 수가 엄청나게 많은 식별자이기도 하여 값을 예측하기 어렵기 때문에 무분별한 데이터 조회를 막을 수 있습니다.
UUID 버전별 특징
v1 | 타임스탬프와 MAC 주소를 기반으로 구성됩니다. |
v2 | DCE 보안 UUID입니다. v1을 기반으로 확장되었습니다. |
v3 | 네임스페이스 ID값에 대한 MD5 해시를 계산하여 생성됩니다. |
v4 | 난수를 기반으로 생성됩니다. 이것이 대부분 사람들이 생각하고 사용하는 UUID입니다. |
v5 | 네임스페이스 ID값에 대한 SHA-1 해시를 계산하여 생성됩니다. |
v6 | v1과 마찬가지로 타임스탬프, MAC 주소 기반이며 시간 순서대로 정렬이 가능합니다. |
v7 | 타임스탬프와 랜덤데이터 기반이며 시간 순서대로 정렬이 가능합니다. |
v8 | 사용자가 정의한 형식에 따라 UUID를 생성합니다. |
출처: RFC 9562 – Universally Unique IDentifiers (UUIDs)
하지만 두 번째 조건인 ‘순서대로’ 조회한다는 v4의 특징으로는 충족하기 힘들었습니다. 랜덤 데이터라 순서를 가지고 있지 않기 때문입니다. 위의 한 줄씩 정리된 버전별 특징을 보면 UUID는 버전이 8까지 존재하고 버전별로 특징이 조금씩 다릅니다. 앞서 설명한 v4는 순서가 보장되지 않기에 저희는 v7을 사용했습니다. v7은 UUID가 타임스탬프 기반의 랜덤 데이터로 생성되고 정렬이 가능하기 때문에 순서를 보장한 랜덤 UUID를 얻어내었습니다. 이렇게 다른 응모작의 고유 번호에 대한 유추가 불가능하며 정렬 가능한 PK를 가지는 데이터 구조를 설계해 보안과 사용성에 영향을 미치지 않는 이벤트를 완성할 수 있었습니다.
마지막으로, 이번 이벤트는 배민선물하기팀에서 처음으로 진행하는 비회원 대상 이벤트였기에 참여자 규모나 트래픽 수준을 정확히 예측하기 어려웠습니다. 이에 2022년 배민신춘문예 당시의 응모 건수(약 53만 건)와 트래픽을 기준으로 준비했습니다. 전시 서버에서 운영 중인 EC2의 총개수와 현재 CPU 및 RAM 사용량을 고려할 때, 이 리소스로 충분히 대응할 수 있다고 판단했습니다. 또한 오픈 후 모니터링을 통해 필요할 경우 Scale-out을 진행할 수 있어 별도의 변경사항은 없었습니다. DB와 Redis는 Scale-up을 통해 OTP 처리 시 최대 1300TPS, 응모작 제출 시 최대 2900TPS까지 견딜 수 있도록 구성했습니다. 이후 예상한 트래픽과 참여자 수 내에서 이벤트를 무사히 마무리할 수 있었으며, 시스템의 안정성도 유지할 수 있었습니다.
참여자들의 생생한 후기
선물카드 제목 짓기 대회가 시작되는 대망의 10월 10일. 그다음 날인 11일 배민 공식 인스타그램에는 선물카드 제목 짓기 대회의 시작을 알리기 위한 게시물이 하나 올라갔습니다. 그리고 그 숏츠는 무려 20만 명이 넘는 분들이 조회했습니다. 감사하게도 모두가 약속이라도 하신 듯 ‘참여완료’라는 댓글이 우수수 달렸습니다. 이어 각양각색의 제목을 제안하는 댓글과 얼마나 많은 작품의 제목을 지어주셨는지를 적어둔 댓글도 달렸습니다. 예상했던 것보다 더 대단한 속도로 응모자가 늘어났습니다. 이때 ‘아마 많은 분들이 이런 이벤트를 기다리셨는지도 몰라.’라고 생각했습니다.
10월 10일부터 11월 3일까지 25일간 진행된 선물카드 제목 짓기 대회에서는 23,583명의 참여자분들이 122,185개의 카드 제목을 지어주셨습니다.
이벤트 기획 당시 예상했던 것보다 훨씬 많은 분들이 응모해 주신 덕분에 총 9명의 심사위원이 투입되었습니다. 심사위원들은 각자 1, 2, 3차를 거치며 꼼꼼히 개인심사를 마치고, 서로가 심사한 내용을 크로스 체크하며 최대한 공정한 심사를 진행하였습니다.
한 카드당 5천 개가 넘는 많은 제목이 접수되었고, 재기 발랄한 제목들을 보며 입꼬리가 슬금슬금 올라가기도 했습니다. 사실 122,185건이 응모되었다는 것이 얼마나 많은 분들이 참여한 것인지 체감하지 못했던 것 같습니다. 하지만 직접 한 작품 한 작품 심사를 하며 확실히 알 수 있었습니다.
그리고 그렇게나 많은 분들이 참가하였는데도 비슷한 제목을 지어주시는 것을 보고 조금 신기하기도 했습니다. 특히 이벤트 응모 당시에 가장 인기가 많았던 흑백요리사와 관련된 작품이 많이 접수되었습니다. 실시간으로 트렌드가 반영되는 모습을 볼 수 있었고, 특히 배민선물하기 이벤트에 참여해 주신 응모자분들의 특성이 이런 유행에 민감한 트렌디한 분들임을 알 수 있었습니다.
이벤트 기획부터 개발, 심사까지도 오랜 시간을 쏟은 이벤트를 마무리하며 문득 ‘과연 이렇게 어렵게 뽑은 수상자분들이 경품을 마음에 들어 하실까?’라는 걱정도 했습니다. 이런 걱정이 무색하게 이번 이벤트에서 시행한 고객 만족도 조사에서는 조사에 참여해 주신 분의 90% 이상이 이번 이벤트가 만족스럽다고 답변해 주셨습니다. 더불어 선물카드 제목 짓기 대회가 무척 배민 다웠으며, 이 같은 참여형 이벤트를 자주 했으면 의견도 받아볼 수 있었습니다.
선물카드 제목 짓기 대회에서는 웃음상, 감동상, 응원상 이 세 부문에서 각 4명의 수상자가 선정되었습니다. 수상자들에게는 웃음상, 감동상, 응원상 이 세 부문에 맞춰 깜찍하게 디자인된 종이 배민상품권 50장이 경품으로 증정되었습니다. 이번 이벤트를 접하신 분들은 아시겠지만 쨍하고 밝은 다홍색의 키 비주얼을 활용하여 상품권을 담을 용도의 박스와 이것들을 모두 담아둘 종이박스도 만들어 정성껏 포장했습니다. (아마 이 글이 발행된 뒤에는 이미 수상자분들이 경품을 품에 안아들고 계실 것 같습니다.)
선물카드 제목 짓기 대회는 고객분들의 의견으로 기존에 있는 카드들에 새로운 글자들로 옷을 입혀주는 것에 큰 의미가 있는 캠페인입니다. 그 의미는 이번 캠페인에 참여해 주신 분들뿐만 아니라 배민선물하기를 이용해 주시는 모든 고객들에게도 닿았던 것 같습니다. 11월 29일 금요일. 마침내 캠페인 수상작 카드의 판매가 시작됐고, 해당 주차 판매량 1, 2위의 자리는 수상작 카드가 나란히 차지했습니다.
판매량 1, 2등을 차지한 선물카드 제목 짓기 대회 수상작들
수상자분들에게는 수상의 기쁨과 귀엽고 특별한 경품이 함께한 추억이 되었기를, 응모해 주신 분들에게는 배민선물하기에 알록달록한 옷을 입혀주신 것과 같은 재미를 느낀 순간이 되었기를 바랍니다.
마무리하며
기획, 개발, 운영, 디자인 모두가 함께한 배민선물하기 제목 짓기 대회는 단순히 이벤트를 넘어, 더 많은 사람들에게 서비스의 특별한 경험과 가치를 전하는 과정이었습니다. 이번 여정을 통해, 배민선물하기만의 진심 어린 선물이 더 많은 사람들에게 다가갈 수 있기를 바랍니다.