디웹시 Blog 검색

관광데이터 활용 공모전 지원기 2편, 차별화와 사용자 연구, 개발 최적화

지정과제 예시와 똑같아진 기획을 어떻게 다르게 만들었는지, 반려가구가 실제로 불편해하는 지점을 어떻게 좁혔는지, 지도 응답 982KB를 어떻게 줄였는지 정리한 관광데이터 활용 공모전 2편이에요.

수정

공유

계획했던 기능을 다 만들고 나니 오히려 불안해졌어요. 관광데이터 활용 공모전 지정과제 6번의 예시 서비스와 제 서비스를 나란히 놓았더니 거의 똑같았거든요. 견종과 무게를 입력하고, 장소 조건을 해석해서, 동반 가능 여부를 보여준다. 출제자가 낸 모범답안을 성실하게 구현한 결과물이었습니다.

이 글은 반려동물 동반 판정 서비스 PetOK을 공모전에 내기까지의 기록 중 두 번째 편이에요. 1편에서는 지원 계기와 과제 선택, 기획 단계의 고충을 다뤘어요. 이번에는 기존 서비스와 어디서 달라야 하는지 고민한 과정, 반려가구가 진짜 불편해하는 지점을 좁혀간 과정, 그리고 실서버에서 응답 속도와 크기를 줄인 최적화 작업을 정리합니다.

관광데이터 활용 공모전 예시 답안과 똑같아진 기획

당시 제가 AI한테 던진 말이 그대로 남아 있어요. “기존의 계획대로 다 해놨는데, 이대로는 기존 공모전 참가작이나 수상작들과 다를 게 없어. 경쟁이 안 될 것 같아.” 기능이 부족해서가 아니라, 기능이 예시와 너무 정확하게 겹쳐서 생긴 불안이었습니다.

문제의 구조는 단순했어요. 관광데이터 활용 공모전은 공사 OpenAPI를 필수로 써야 하고, 6번 과제에는 반려동물 동반여행 API라는 정답 데이터가 있습니다. 6번을 고른 팀은 전부 같은 API에서 같은 1,048건을 받아서 같은 조건 문장을 파싱하게 돼요. 데이터가 같으니 데이터에서는 차이가 날 수가 없었습니다. 찾아보니 같은 공사 API로 이미 운영 중인 반려동물 장소 서비스도 있었어요. 장소 수와 리뷰 수로 경쟁하는 서비스였는데, 그 방향으로 가면 신생 공모전 출품작이 이길 방법이 없었습니다.

그래서 포지셔닝부터 다시 정했어요. PetOK은 장소 목록 서비스가 아니라 조건 해석 서비스다. “어디에 갈 수 있나”를 많이 보여주는 게 아니라, “우리 애가 여기 정말 들어갈 수 있나”를 정확하게 답하는 쪽으로요. 장소 수로는 못 이겨도, 답의 정확도와 정직함으로는 다르게 보일 수 있다고 판단했습니다.

차별화 아이디어를 AI와 같이 뽑아보니 꽤 많이 나왔어요. 견종 특성으로 “갈 만한가”를 판정하는 두 번째 축을 만들자, 날씨 API로 아스팔트 온도를 추정해서 발바닥 화상 위험을 알려주자, 도착하면 현장 모드로 바꿔서 직원에게 물어볼 문장 카드를 띄우자, 사용자에게 결측 데이터를 채우는 미션을 주자. 하나하나 들으면 그럴듯했습니다.

그런데 대부분 버렸어요. 이유는 크게 두 가지였습니다. 첫째, 화면이 복잡해졌어요. AI는 심사위원을 설득할 논리를 사용자 화면에 그대로 밀어 넣으려는 경향이 있었고, 저는 그때마다 선을 그었습니다. 심사위원이 좋아할 설명과 사용자가 매일 볼 화면은 다른 문제니까요. 둘째, 뻔했어요. 결측을 사용자가 채우게 하자는 제안을 듣고 “이것도 너무 뻔하다, 킥이 없을까”라고 되물었던 기억이 납니다. 어느 서비스나 하는 걸 공모전에서 차별화라고 내밀 수는 없었어요.

지도 홈 자체를 다시 생각해보자는 제안도 나왔는데, 이건 제가 처음부터 직접 고른 방향이라 거기서 확실히 정리했어요. 반려가구가 외출할 때 하는 질문은 결국 “이 근처에 갈 수 있는 데가 어디야”이고, 그 질문에 제일 빨리 답하는 화면은 지도라고 봤습니다. 그 뒤로는 지도를 바꾸는 게 아니라, 지도 위에서 어떻게 다르게 보여줄지로 논의가 옮겨갔어요.

이 밖에도 접은 것들이 있어요. 인스타그램 DM으로 장소에 자동 문의하는 기능은 플랫폼 이용약관 위반이라 바로 뺐고, AI 전화로 장소에 직접 확인하는 방식은 비용과 운영 부담 때문에 우선순위에서 내렸습니다. 방문 기록을 일정 개수 이상 보려면 로그인하게 만드는 블러 게이트도 고민했는데, 사용자가 거의 없는 초기에는 발동할 일도 없는 복잡한 장치라 접었어요. 결론은 “보는 건 전부 열려 있고, 남기려면 로그인”입니다. 조건이 맞았는지 틀렸는지 투표하는 건 비회원도 할 수 있게 열어두고, 나중에 로그인하면 기록이 합쳐지게 했어요.

반려가구가 진짜 불편한 지점 찾기

차별화의 재료는 결국 사용자 쪽에서 나왔어요. 1편에서 쓴 지인 설문에서 동반 가능이라더니 아니었던 경험으로 나온 답이 “테라스만 가능”, “테라스가 많았음”, “추가 요구사항이 있었다”였습니다. 반려가구가 화나는 순간은 “동반 불가”를 미리 알았을 때가 아니라, “동반 가능”을 믿고 갔는데 조건이 붙어 있었을 때였어요.

이걸 데이터와 맞대보니 정확히 겹쳤습니다. 공사 데이터에서 “일부구역 동반가능”으로 분류된 곳의 62.6%가 어느 구역인지 적혀 있지 않았거든요. 사용자가 겪는 배신감의 상당 부분이 데이터의 빈칸에서 나오고 있었던 거죠. 그래서 판정 단계 중 “확인 필요”를 숨기지 않고 지도에 그대로 보여주기로 했습니다. 모르는 걸 “가능”으로 뭉개는 순간 그 서비스도 헛걸음을 만드는 쪽이 되니까요.

규정 자체가 단순하지 않다는 것도 확인했어요. 식약처 설명자료에 따르면 반려동물 동반출입 음식점 기준을 담은 식품위생법 시행규칙이 2026년 1월 2일 개정되고 3월 1일부터 시행됐습니다. 관련 보도를 보면 출입 가능한 동물은 개와 고양이로 한정되고, 영업자는 맹견의 출입을 제한할 수 있고, 예방접종을 안 한 반려동물은 출입이 제한된다는 표시도 해야 해요. 음식점 동반이 제도적으로 열렸다고 해서 “가능” 한 단어로 끝나는 문제가 아니라는 뜻입니다. 같은 가게라도 우리 집 반려견이 맹견인지, 접종을 했는지에 따라 답이 달라져요.

그래서 반려동물 프로필 입력을 최소화하는 쪽으로 설계했어요. 판정에 꼭 필요한 건 맹견 여부 하나만 필수로 받고, 견종을 고르면 크기 분류는 자동으로 붙게 했습니다. 체중, 크기를 하나하나 입력하게 만들면 처음 들어온 사람은 판정을 보기도 전에 나가거든요. 판정 색은 초록, 노랑, 빨강 세 가지 계열로 단순하게 묶되 아이콘을 같이 붙여서 색을 구분하기 어려운 분들도 읽을 수 있게 했어요.

PetOK 장소 상세 화면에서 판정 결과와 근거가 되는 원문 조건이 함께 보이는 모습

최종적으로 남긴 두 가지 차별화

수많은 아이디어 중 끝까지 남은 건 두 개였어요.

첫 번째는 사용자 제보에 증거 수준별로 무게를 다르게 주는 구조입니다. 현장 사진이 있는 제보, 메모만 있는 제보, 아무 증거가 없는 제보는 신뢰도가 다르잖아요. 장소 가까이에서 남긴 제보는 더 믿을 만하고요. 그래서 제보마다 증거 등급에 따라 가중치를 다르게 매기고, 약한 제보는 더 많은 동의가 모여야 판정을 바꿀 수 있게 했어요. 방문 인증을 필수로 걸면 입장을 거절당한 사람의 제보가 사라지는 문제가 있었는데, 이 구조라면 거절 제보도 버리지 않으면서 장난 제보 한 건에 판정이 흔들리지 않습니다.

두 번째는 임팩트 추적이에요. 지정과제 문구 자체가 “헛걸음 유발 문제”인 만큼, 심사위원이 결국 물을 질문은 “그래서 헛걸음을 얼마나 줄였는가”라고 봤어요. 판정이 언제, 어떤 근거로 바뀌었는지 이력을 남기고, 바뀐 이후 그 장소가 몇 번 조회됐는지 집계해서 /impact 페이지에 산식까지 공개했습니다. 이력이 없으면 없다고 표시하고요. 운영자가 직접 확인해서 넣은 데이터는 일반 사용자 제보와 화면에서 반드시 구분되게 했어요. 숫자를 부풀리지 않는 것까지가 이 기능의 일부라고 생각했습니다.

데이터 소스도 하나 늘렸어요. 반려동물과 캠핑장을 같이 찾는 수요가 크다고 봐서 고캠핑 API를 붙였는데, 반려동물 동반 필드를 실측해보니 불가능 56.2%, 빈값 23.6%, 가능 20.2%였습니다. 여기서도 빈값은 “가능”이 아니라 “확인 필요”로 매핑했어요. 걷기 여행길 데이터도 검토했지만 반려동물 관련 필드가 없어서 보류했습니다. 고캠핑을 붙이고 나니 실서버 기준으로 캠핑장만 3,128곳, 전체 판정 장소는 약 4,150곳이 됐어요. 그중 853곳이 확인 필요로 분류됐는데, 이 숫자를 줄이는 게 앞으로 서비스가 해야 할 일이라고 생각하고 있습니다.

PetOK 임팩트 페이지에서 판정 분포 스택바와 공개된 산식

Cursor 협업과 실서버 최적화

이 모든 걸 혼자 만들려면 AI 에디터를 잘 다뤄야 했어요. 개발은 Cursor로, 기획 정리와 작업 지시서 작성, 코드 검토는 Claude로 나눴습니다. 혼자 일할 때 제일 무서운 건 어제 정한 원칙을 오늘의 AI가 잊어버리는 거예요. 그날 작업을 통째로 다시 해야 하거든요.

그래서 코드보다 문서를 먼저 깔았어요. 프로젝트 규칙 파일(.cursor/rules/petok-core.mdc)과 스펙 문서에 절대 원칙을 적었습니다. 빈 데이터와 허용을 섞지 않는다, 공사 원문은 가공하지 않고 보존한다, 사용자 요청 경로에서 외부 API를 직접 부르지 않는다 같은 것들이요. 작업은 마크다운 티켓 단위로 넘겼고, 티켓마다 이 원칙을 제약 조건으로 반복해서 넣었어요. 수용 조건에는 “빠르게”가 아니라 측정 가능한 숫자를 적었습니다. 숫자가 없으면 AI도 저도 언제 끝났는지 판단할 수 없더라고요. 관광데이터 활용 공모전처럼 마감이 정해진 작업에서는 이 방식이 특히 도움이 됐어요. 티켓 하나가 끝날 때마다 수용 조건의 숫자를 다시 재보면, 어디까지 됐는지 헷갈릴 일이 없었습니다.

스택은 Next.js 16 App Router, React 19, TypeScript에 PostgreSQL과 PostGIS를 쓰고, 지도는 네이버 지도 API v3, 로그인은 카카오, 이미지 저장은 Cloudflare R2로 갔어요. 개발 단계에서는 관리형 DB를 썼지만 운영은 원래 쓰던 VPS에 DB까지 같이 올렸습니다. nginx와 PM2로 띄웠고, 다른 앱 7개가 같이 돌고 있는 독일 리전 서버예요.

배포 전에는 실서버를 직접 측정해봤어요. 감으로 최적화하면 엉뚱한 데 시간을 쓰기 쉬워서요. 이번에는 Claude를 크롬 브라우저에 연결해서 실제 서비스 페이지를 열어둔 채로 자바스크립트로 요청 구간별 시간을 찍게 했습니다. 개발자 도구를 하나하나 눌러보는 것보다 같은 조건으로 여러 번 반복 측정하기가 훨씬 편했어요. 측정 결과를 보고 판단은 제가 하는 식으로 역할을 나눴습니다.

첫 응답이 1초 가까이 걸렸어요. 처음엔 서버가 독일에 있어서 그런 줄 알았는데, 구간을 쪼개 보니 대부분이 TLS 핸드셰이크 비용이었습니다. 연결이 한 번 맺어진 다음의 응답은 320ms 정도였어요. 싱가포르 리전으로 옮기면 어떨지 핑도 재봤는데, 독일 261.6ms에서 싱가포르 119.6ms로 줄긴 했습니다. 그래도 마감 전에 서버를 옮기지 않기로 했어요. 7개 앱이 같이 도는 서버를 옮기는 리스크에 비해 얻는 게 적었고, 더 큰 낭비가 따로 있었거든요.

그 낭비는 응답 크기였어요. 지도를 전국 범위로 줌아웃하면 응답이 982KB였습니다. 필드별로 쪼개 보니 썸네일 URL이 190KB, 장소 요약 문구가 85KB를 차지하고 있었어요. 지도 핀을 찍는 데는 둘 다 필요 없는 데이터라, 목록 응답에서는 빼고 상세 화면을 열 때만 불러오게 바꿨습니다. 서버 이전 없이도 체감 속도를 올릴 수 있는 쪽이었어요. Cloudflare 캐시가 안 먹고 있던 원인도 찾았어요. 응답 헤더의 Vary 값이 너무 넓게 잡혀 있어서 요청마다 캐시가 따로 갈라지고 있었습니다. 같은 지도 범위를 보는 사용자끼리는 응답을 공유할 수 있게 헤더 범위를 좁혀서 정리했어요.

측정하다가 버그도 하나 잡았어요. 지도 범례에는 “갈 수 없음”이 있는데, 지도 응답에는 그 장소들이 아예 빠져 있었습니다. 원래는 갈 수 없는 곳을 지도 핀에서 숨기는 방향으로 정했었는데, 이걸 뒤집었어요. 못 가는 곳을 미리 아는 것도 헛걸음을 막는 정보잖아요. 그래서 갈 수 없는 곳도 모든 줌 레벨에서 보여주고, “갈 수 없음” 필터 칩을 추가하되 기본값은 켜둠으로 했습니다.

클러스터 마커도 바꿨어요. 원래는 묶인 장소들 중 다수인 판정 색 하나로 칠했는데, 이러면 갈 수 없는 곳이 섞여 있어도 초록으로 보일 수 있습니다. 그래서 판정 비율을 링 형태로 나눠서 보여주게 했고, 색은 /impact 페이지의 스택바와 같은 규칙을 썼어요. 위치 정보는 사용자의 GPS 좌표 원본을 서버로 보내지 않고, 지금 보고 있는 지도 화면 범위만 보내도록 정했습니다.

PetOK 지도에서 판정 비율이 링 형태로 나뉜 클러스터 마커와 필터 칩

차별화에 대한 흔한 오해

공모전 차별화를 기능 개수 싸움으로 보는 경우가 많아요. 남들이 다섯 개 만들 때 열 개 만들면 이긴다는 식으로요. 이번에 겪어보니 반대에 가까웠습니다. 같은 데이터, 같은 과제를 가진 팀들 사이에서 기능을 얹을수록 화면은 복잡해지고, 서비스가 뭘 하는지 한 문장으로 설명하기 어려워졌어요. 제가 결국 남긴 건 판정을 정직하게 하는 것, 그 판정이 얼마나 효과가 있었는지 숫자로 공개하는 것, 이 두 가지였습니다.

또 하나는 “모르는 데이터는 숨기는 게 깔끔하다”는 생각이에요. 데이터가 비어 있는 장소를 지도에서 빼면 화면은 깔끔해 보입니다. 그런데 사용자는 그 장소가 왜 안 보이는지 모르고, 검색으로 찾아가서 결국 헛걸음을 해요. 관광데이터 활용 공모전 6번 과제가 해결하라는 게 바로 이 헛걸음이니, 모르는 건 모른다고 보여주는 게 과제에 대한 정답에 더 가깝다고 판단했습니다.

2편을 정리하면

이번 관광데이터 활용 공모전에서 차별화를 만든 건 새 기능이 아니라 기준이었어요. 장소를 많이 보여주는 서비스와 경쟁하지 않고, 조건을 정확하게 해석하는 서비스로 위치를 옮겼습니다. 사용자가 진짜 화나는 지점은 “동반 가능”을 믿었다가 조건에 막히는 순간이었고, 그 원인이 데이터의 빈칸에 있다는 걸 숫자로 확인했어요. 최적화도 같은 방식이었습니다. 감으로 서버를 옮기기 전에 재보고, 제일 큰 낭비부터 걷어냈어요.

다음 3편에서는 실서버 테스트와 배포, 1차 심사용 기능설명서를 만들고 제출하기까지의 과정, 그리고 혼자 이 공모전을 준비하면서 느낀 점을 정리할게요.

이어서