웹사이트의 첫 삽부터 나무를 기르기까지: 당근닷컴 디벨롭의 여정

JungHyun Lah
당근 테크 블로그
25 min readMar 25, 2024

--

리뉴얼 된 글로벌 웹사이트

처음 당근에 들어오기 전, 저는 당근을 사용하는 1800만 유저 중 한 명이었어요. 중고거래 뿐만 아니라 지금은 동네모임까지 생긴 동네생활을 사용하며 이웃들과 도움을 주고 받고 따뜻한 마음을 느끼며 누군가의 이웃이 되는 그 행복이 저를 당근으로 이끌었네요. 저는 프론트엔드코어 팀의 웹사이트 파트에서 당근의 콘텐츠를 웹에 올리고 퍼뜨리고 있는 프론트엔드 개발자 lea.lah 입니다.

당근에 웹사이트가 있다는 것, 알고 계셨나요? 많이 접속해보셨을 기업 소개 사이트 외에도, www.daangn.com/ 이라는 콘텐츠 중심의 웹사이트가 있답니다. 이제는 중고거래 뿐만 아니라 동네 업체, 알바, 부동산, 중고차, 그리고 곧 올라갈 모임까지 다양한 당근의 콘텐츠가 올라가 있어요. 그리고 꽤 많은 유저가 웹을 방문하고 있답니다.

월 140만 명이 오가는 당근닷컴

당근은 앱이 메인인 회사이기 때문에, 웹의 필요성이 대두 되기까지 꽤 오랜 시간이 걸렸어요. 이 글에서는 어떻게 초기 웹사이트가 개발되었고, 어떤 부분들을 챙겼으며, 어떻게 리뉴얼까지 이르렀는지 전반적인 내용을 담아보고자 해요. 앱에서 뿐만 아니라 웹으로 검색하는 유저도, 앱은 없지만 이웃, 지인, 가족으로부터 공유받은 당근의 콘텐츠를 열어보는 유저도 모두 접하고 탐색할 수 있는 당근닷컴을 한번 구경해보실래요?

https://karrotmarket.com/?in=toronto-11052

인터넷을 쓴다면 웹은 필요해요.

당근은 MAU 1,800만 명이라는 큰 숫자의 유저를 보유하고 있어요. 하지만 몇 년 전까지만 해도, 어느 검색 사이트에서 검색하든 중고거래 게시글을 제외한 당근의 콘텐츠(알바, 부동산, 중고차, 동네업체)는 나오지 않았어요. 앱에 많은 콘텐츠가 갇혀 앱이 없는 유저는 공유된 글이 어떤 글인지 확인하지도 못했죠. 무조건 앱을 다운로드하거나 안 보거나, 하는 모 아니면 도의 선택지만 주어졌어요.

하지만 다양한 서비스가 생기며 점점 빠르게 커져가는 당근의 생태계에서 콘텐츠가 앱에만 갇히는 건 큰 손실이에요. ‘난 굳이 앱까지 다운받고 싶지 않은데?’, ‘그냥 글만 보여주면 되지 왜 다운까지 받아야 해’ 하며 이탈하는 유저를 잃게 되고, 이탈한 유저들은 “당근”을 매개체로 한 지역 기반 커뮤니티를 모르게 되기 때문이죠. 우리가 얼마나 다양한 서비스를 운영하고 있고, 따뜻한 당신 근처의 이웃들을 만들기 위해 어떤 커뮤니티를 만들고 어떤 시도를 하고 있는지 유저에게 아예 보여줄 수가 없는 거예요.

첫 삽은 그렇게 뜨기 시작했어요. World Wide Web에 콘텐츠를 올리자. 당근을 모르고, 평소 관심이 적었던 유저들에게 우리의 커뮤니티를 보여주자. 그럼 우리 플랫폼에 공감할거야. 그렇게 웹에 글을 노출하기 위해 URL을 따고 페이지를 만들기 시작했어요.

코어 팀의 To-do와 Not To-do는 무엇일까?

여기서 제가 속한 팀에 대한 소개를 해볼게요. 저희 팀은 프론트엔드 개발자들로 이루어져 있어요. 당근은 목적 조직이지만, 저희 팀은 Tech Core 조직으로 기능 조직과 같이 일하고 협업해요. 그럼 왜 각 팀에서 웹을 개발하지 않고 코어팀에서 하는 거지? 라고 생각하셨다면, 잘 따라오신 거예요. (굿..) 웹은 보이지 않는 뒷단에서 돌아가는 많은 정책이 필요해요. 하지만 목적 조직으로 이루어져 있고, 자유롭고 다양한 기술의 채택을 장려하는 당근의 개발 문화에서 하나의 통일된 정책을 만드는 것은 높은 우선순위를 가지진 않았어요. 그래서 처음 웹사이트를 제작하며 저희가 코어팀으로써 정해나간 부분은 URL 규격이었어요. (일명 ‘퍼머링크’)

퍼머링크 URL path 규격을 정책화하기 위한 RFC

퍼머링크는 변하지 않는 고유한 id로서의 링크를 의미해요. 물론 변경이 불가한 건 아니지만, ID로서 존재하는 링크이기 때문에 변경된다면 특정 데이터를 고유하게 가리키는 identifier가 될 수 없어요. 따라서 최대한 변경될 여지가 있는 속성을 제거해야 했어요.

저희가 가져간 규격을 간단하게 정리하면 아래와 같아요. [스펙]

[도메인]/[국가코드]/[언어(생략가능)]/[콘텐츠타입]/[타이틀]-[publicId]/

여기서 도메인과 타이틀은 id에서 제외되고, /[국가코드]/[언어(생략가능)]/[콘텐츠타입]/[publicId]/ 이 id이자 canonical url에 사용돼요. 이 중 변경될 여지가 낮은 것부터 나열해보면 아래와 같아요.

  1. 변경 가능성 없음: ISO 3166–1에 따른 국가 코드/ ISO 639–1에 따른 언어
  2. 변경 가능성 존재: 회사/ 팀 단위로 관리하는 publicId
  3. 변경 가능성 농후: 콘텐츠의 세분화 또는 통합이 반영되는 콘텐츠 타입

첫 번째, 당근은 글로벌에 진출했기 때문에, 각 국가마다 홈페이지가 존재하고 현지 언어를 지원해야 해요. 당근의 글로벌 시장 규모를 고려했을 때, 현재는 한국을 제외한 국가의 경우 중고거래만 운영하고 있기 때문에 각 국가의 대표적인 언어 만을 지원하는 것으로 충분하다 판단했어요. 따라서 언어 코드는 생략하고 운영되고 있는 국가의 국가 코드만 포함해요.

두 번째, publicId의 경우 목적 조직 특성상 각 팀에서 서비스의 데이터 id를 각기 다른 체계로 관리하고 있기 때문에, 이를 통합해 관리하는 데 들어갈 리소스와 각 팀에서 기존 database id와의 의존성을 줄인 public id 생성에 들어갈 리소스를 비교했어요. 이후의 유지 보수성까지 고려해 public id를 생성하는 것으로 결정했죠.

세 번째, 콘텐츠 타입의 경우 당근의 콘텐츠를 어떤 계층에서 볼 것인지가 추후 변경 가능성을 크게 낮출 수도, 높일 수도 있어요. 쉽게 콘텐츠 타입 === 서비스명 으로 보면 되지 않나? 싶을 수 있지만, 변경이 잦고 큰 조직이라는 점을 감안하면, 서비스명이 변경되었을 때 콘텐츠 타입의 변경도 필요해요. 그럼 또 다른 id가 파생되는 거죠. 그래서 콘텐츠에 대한 정의를 최대한 로우 레벨에서 명시적으로 정의하도록 했어요. 동일한 서비스에서 여러 개의 콘텐츠 타입이 생성될 수 있는 거죠.

이후 용이한 관리를 넘어, 퍼져 있는 당근의 콘텐츠 및 스키마를 한눈에 파악할 수 있는 graph를 만드는 것을 큰 그림으로 두고, GraphQL을 채택하여 모든 팀의 최신/ 인기 콘텐츠들을 가져오는 API 하나, id 배열을 전달해 콘텐츠들을 가져오는 API 하나 총 2개의 API를 받아왔어요. 그리고 graphql-mesh 를 사용하여 하나의 실행 가능한 서버 스키마로 schema stitching을 하고, 클라이언트에서 사용하기 위해 클라이언트 스키마를 정의한 뒤, 서버 스키마에서 받아온 데이터를 클라이언트 스키마에 맞추는 리졸버를 작성하고 Next.js에서 해당 데이터를 사용할 수 있도록 구성했어요.

당근의 콘텐츠 종류를 한눈에 살펴볼 수 있는 스키마 폴더

다만 프론트엔드 코어팀으로서 각 팀의 데이터와 화면 단위를 모두 작업하기보다, 웹페이지를 제작하는 프레임워크를 만들고, 작업은 각 팀에 위임했어요. 각 팀의 화면, 그 구성의 의도나 중요한 부분들을 모두 파악하기에는 한계가 존재했기 때문이에요. 이 프레임워크는 schema, resolver, renderer 로 구성하고, 추가적으로 용이하게 사용될 수 있는 공통 컴포넌트와 이후 웹과 앱을 연결하는 부분에서 사용된 워커로 이뤄졌어요.

웹사이트 개발을 위한 모노레포 구성

보이지 않던 뒷단의 일들

유니버설 링크와 딥 링크 설정하기

첫 타자로 알바팀의 콘텐츠가 나가고, 이어서 부동산, 중고차 모든 버티컬 서비스가 나감과 동시에 추가적으로 정책이 필요한 부분이 생기기 시작했어요. 기존 중고거래 사이트는 유니버설 링크 등의 설정 없이, 모든 페이지에 접근 시 앱을 열도록 강제하고 있었어요. 하지만 이건 사용자 경험에 크게 좋지 않아요. 동일한 사이트를 접속하는데도 매 페이지마다 앱을 열도록 모달을 띄웠으니까요. 이를 해결하기 위해 유니버설 링크와 딥링크를 등록하는 일이 필요해요. 이를 위해서는 루트의 아래 파일에 path 규칙을 등록하면 되는데요.

/.well-known/apple-app-site-association
/.well-known/assetlinks.json

이렇게 링크를 등록하면, 유저가 등록된 링크로 접속했을 때 (iOS라면) 앱이 있는 경우에는 해당하는 앱스킴으로 이동시키고 (OS마다 동작이 다를 수 있어요. 예를 들어 안드로이드의 경우 Google Play Store로 이동할지, 앱으로 이동할지 등의 선택지를 제공해요.), 없는 경우에는 웹에 머무를 수 있어요. 무조건 앱 설치 페이지로 이동하지 않고 앱 다운로드 없이 웹에서 탐색할 수 있게 되죠. 또한, html tag를 사용하여 앱으로 이동할 수 있도록 스마트앱 배너를 추가하면, 아래와 같이 상단 배너가 뜨게 돼요.

<meta
key="apple-itunes-app"
name="apple-itunes-app"
content={`app-id=${APP_ID}, app-argument=${val}`}
/>
웹페이지 상단에 보이는 스마트 배너

당근의 각 팀들은 모두 다른 앱스킴 path를 가지고 있어요. 또한 앱스킴에는 웹에 노출되는 public id가 없기 때문에 이 둘을 매핑하는 과정이 필요해요. 따라서 웹과 앱 콘텐츠를 매핑하기 위해 워커를 띄워 public id과 앱스킴을 매핑시켜 주어야 해요. 이를 위해 x-property로 워커 엔드포인트를 등록하고, 네이티브에서 유니버설 링크로 등록된 링크에 접속하는 경우 해당 링크를 body로 POST 요청을 보내요. 그럼 워커에서 링크로부터 public id를 파싱하고, 서비스에 해당하는 app과 path를 조립하여 네이티브에 다시 전달하여 앱스킴을 열도록 했어요.

테스트를 위해 아래 링크를 눌러보시면, 앱이 있는 경우 앱의 해당 콘텐츠로 이동할 거예요. 없다면 웹에 머물 거고요.

당근 고객센터 단기계약직 모집(실업급여 가능) — 서울특별시 구로구 구로동 | 당근알바

앱이 없는 유저가 앱 다운로드를 원치 않으면 웹에 머물 텐데, 그럼 접속한 하나의 페이지뿐만 아니라 다른 콘텐츠도 탐색이 가능하도록 유도해야 하죠. 그리고 그 콘텐츠들을 모두 볼 수 있는 홈이 필요해요. 그렇게 당근닷컴에 대한 니즈는 커져갔어요. 단순히 퍼머링크를 만들고 페이지를 제작하는 것만 필요한 게 아니라, 이걸 담을 큰 그릇을 잘 만들어야 했죠. (이런 걸 고구마 줄기를 뽑았다고 칭하기도 해요..~)

서브 도메인 정리하기

이전에 중고거래, 동네생활과 동네정보는 서브도메인이 daangn.com/town.daangn.com/ 으로 분리되어 있었어요. 하지만 이 둘 모두 관리되고 있지 않아 코어팀에서 퍼머링크를 만들고 당근닷컴에 다른 서비스를 올리면서 town.daangn.com/ 의 콘텐츠도 모두 당근닷컴으로 옮겨오게 되었어요. 하지만 이미 반복적으로 크롤링되어 인덱싱 된 페이지였기 때문에 검색 결과에 노출되고 있고, 다른 사이트와 링킹되어 (링크를 다른 사이트에서 포함하고 있는 등) 상위에 노출되고 있었기 때문에 이 결과를 유지하며 옮겨와야 했어요. 따라서 당근닷컴의 “동네업체” 탭으로 redirection 처리를 하면서 기존에 나가있던 페이지가 유실되지 않도록 구글 서치콘솔과 빙 웹마스터 등의 도구를 사용하였어요.

Google Search Console 결과; 이제는 Indexing 되고 있지 않은 예전 town.daangn.com 링크들

또한 당근닷컴으로 배포된 URL을 canonical url (참고자료로 설정하면서 동일한 페이지를 가리키는 여러 URL 중 검색봇에게 알려주는 대표적인 URL을 변경했어요.

서로 다른 프로젝트 연결하기

기존 daangn.com/ 은 Ruby on Rails 로 작업된 프로젝트에서 배포되고 있어요. 하지만 프론트엔드코어팀에서는 별도의 레포지토리에서 작업되어 하위 path인 /kr/* 로 배포해야 했기 때문에, 이 둘의 연결이 필요했어요. 이를 위해 nginx 로 띄운 프록시 서버에서 daangn.com/kr/* 의 path 에 대해서는 코어팀에서 작업한 프로젝트 결과물이 연결되도록 SRE팀과 협업하였어요.

동일한 도메인에 대한 sub-path 라우팅하기

지금 보이는 일부 국내 당근 콘텐츠의 URL이 /kr/* 포맷이 아니라면, /kr 이 없는 콘텐츠는 레일즈로, /kr 이 있는 콘텐츠는 리액트로 작업되어 있답니다.

추가적으로, /kr/* 외에도 Next.js 를 사용하면서 빌드 결과물이 담기는 /_next 하위의 에셋들도 코어팀의 프로젝트에서 가져가야 해요. 또한 manifest와 이에 포함되는 icon 파일도 포함되어야 하죠. 이를 위해 /_${project_name} 으로 이름을 세팅하고 모두 해당 프로젝트에서 서빙하도록 세팅하여 두 프로젝트에서 동일한 도메인에 대해 서빙할 수 있게 되었어요.

하지만 동일한 도메인에 두 프로젝트가 연관되는 것과 별개로, 이 둘은 결국 동일한 사이트를 보여줘요. 이 말은 즉, 동일한 사이트가 보여줘야 하는 많은 공통적인 요소들에 대해 두 벌의 작업이 필요해진다는 것이죠. ‘그럼 중고거래도 마이그레이션 해오면 되지 않나’, 하신다면 이 글을 작성하는 현재로썬 진행 중이니 리뉴얼될 당근닷컴에 많은 관심 부탁드려요! 😇

중복을 방지하기 위해 찾은 방법은 코어팀에서 새롭게 작업한 GNB, Footer 와 같은 큼직한 공통 컴포넌트를 빌드하고 이 결과물을 레일즈에서 읽어가는 방식이었어요. 이때 기존 웹사이트는 라이트 모드만 지원하는 점을 감안해 다크모드도 지원하는 새로운 작업물들에 디자인 시스템의 light-mode css 를 입히고 esbuild로 빌드한 HTML, CSS, JS 를 AWS S3에 commit id를 이름으로 한 버킷으로 업로드 한 뒤, 레일즈 프로젝트에서 이 S3 버킷을 가져가 사용하였어요.

이렇게 동일한 페이지에서 여러 배포건이 종합되어 보여지다보니 일부는 잘 보여지는 동시에 일부는 잘 보이지 않는 문제가 발생하기도 했어요. 일례로 S3에 업로드 되는 파일명을 변경하고 섣부르게 배포하면서 레일즈에서 이를 읽어가지 못했고, 아래와 같이 GNB만 보이지 않는 장애가 발생해 첫 장애일지를 작성하기도 했답니다.

vercel 배포 건을 읽어가지 못해 생긴 GNB 에러
이에 대해 작성한 장애일지

이뿐만 아니라 CSS 가 말썽을 부려스타일이 적용되지 않던 문제도 있었어요. 저희는 Vanilla Extract라는 CSS-in-JS 라이브러리를 사용하고 있었는데, 이는 정적으로 CSS 파일을 빌드하면서 className 을 생성하고 사용해요. 하지만 레일즈에 전달하기 위해 빌드한 GNB, Footer의 className이 겹치면서 의도한 스타일이 적용되지 않았고, 이를 해결하기 위해 GNB, Footer 에 id 를 지정한 뒤 VE이 생성한 클래스명에 postcss 로 id 값을 namespace로 한 새로운 클래스명을 생성하여 className으로 적용되게 변경했어요. 그 결과로 당근닷컴에서 모두 동일한 컴포넌트가 보일 수 있게 되었어요.

퍼포먼스 개선하기

당근닷컴은 Next.js의 Static Site Generation으로 작업되어 있어요. SSG를 채택한 이유는 다음과 같아요.

  • 웹 버전 당근의 콘텐츠는 fresh하게 관리되는 것보다 유저에게 빠르게 서빙되는 것이 중요하다는 점
  • 정적 콘텐츠 외의 속성이 추가되면서 요구사항이 걷잡을 수 없이 확산되는 경우를 방지할 것
  • 코어팀에서 schema stitching을 하여 각 팀의 API를 사용하는 것이기 때문에 SSR 사용 시 즉각적인 서버 대응이 어렵다는 점

하지만 콘텐츠가 늘어가고 그만큼 서버에 요청하는 request 수가 많아질수록 성능은 나날이 안 좋아졌어요. 미리 정적 페이지를 빌드해 놓고 1시간 동안 캐시해두어 사용자로 하여금 빠른 페이지 로드를 경험할 수 있게 함이 목표였으나, 로딩 속도가 굉장히 느려 여러 팀에서 CS를 받기도 했죠. 일단 원인을 파악하기 위해 webpagetest로 테스트한 결과, 초기 에셋 로딩 시간이 일정하지 않고 불규칙하게 튀는 현상을 발견했어요. 이를 해결하기 위해 Next.js 서버에서 빌드된 아티팩트를 별도로 분리하여 이러한 정적 파일을 서빙하는 서버를 fastify/static을 이용해 서빙하도록 하고, next.config.js 에서 nextPluginassetPrefix 에 해당 서버의 엔드포인트를 지정해 정적 파일을 서빙하는 서버를 분리시켰어요. 그 결과 불규칙하게 튀던 시간이 어느 정도 일정하게 맞춰졌고, 로딩 시간을 약 5초 정도 줄일 수 있었어요. (5초나 줄었다는 것은 그전에 로딩 시간이 지나치게 길었다는 의미이기도 해요.) 아래 이미지로 확연하게 달라진 waterfall 양상을 확인하실 수 있어요. (화질이 깨지지만, 전체적인 양상을 확인해보세요!)

Next.js 로 static asset 서빙했을 때
별도의 서버를 띄워 static asset을 서빙했을 때

첫 줄기는 글로벌로

이렇게 다양한 서비스를 올리고 웹사이트에 점점 많은 콘텐츠가 노출될 수록, 서비스 별로 보여주고자 하는 기능과 요구사항이 분화되고, 크게는 레이아웃부터 작게는 실시간 정보 반영까지 웹에 대한 목소리가 다양해졌어요. 이뿐만 아니라 콘텐츠 별로 다르게 적용되는 SEO 전략 또한 목적 조직에서 관리하기보다 코어팀에서 웹사이트의 전체적인 지표를 정리하고 전략을 수정하는 등, 적절한 웹의 전략과 방향성에 대해 고민하는 것이 필요했어요. 이를 위해 기존에 목적 조직에서 기여를 하고 프론트엔드 코어팀이 중추 역할을 하던 방식에서 웹사이트에 대한 오너십을 온전히 가져오는 방향으로 변경하고자 구성원을 설득해야 했죠. 따라서 기존보다 발전한 웹 버전이 필요했고, 시기 적절하게 글로벌 웹사이트 개편 작업을 시작하게 돼요.

이는 당근닷컴을 넘어 글로벌에서 사용되고 있던 karrotmarket.com 도메인에 대한 관리 주체도 넘어오는 것이었기 때문에, 퍼머링크와 별개로 도메인에 대한 고민도 필요했어요. 국내와 글로벌이 다른 도메인을 사용하고 있었기 때문에, 웹사이트에 대한 오너십이 넘어온 지금 이 둘을 모두 개발하고 관리하는 입장에서 추후 도메인의 통합 가능성을 고려해야 했죠. 현재 한국의 경우 ‘처’의 의미로 당근이라는 상호명을, 외국에서는 karrotmarket을 사용하고 있어요. 국내에서는 “당근” 이라는 상호명이 함축적이고 하나의 브랜드로써 통칭되나, 외국에서는 그렇지 않아요. ‘daangn 이 뭔데?’ 할 수 있는 거죠. 하지만 중고거래만 운영되고 있는 글로벌을 생각했을 때 market 을 제외한 karrot 만으로 표기하는 것도 의미를 전달하기에 적절하지 않았어요. 또한 .com 에 대한 의견도 여럿 있었는데, commercial 을 넘어 다양한 서비스를 포함하는 특성을 고려해 다른 도메인을 고민했으나 여러 브랜딩 요소가 고려되어 다른 도메인명은 모두 opt-out 되었어요. 정리하자면, karrotmarket.com 으로 통일하되 karrotmarket.com/kr 의 경우 daangn.com 으로 운영하도록 정리되었어요. (추후 글로벌에서도 국내처럼 다양한 서비스가 올라간다면 변경될 수 있겠죠?)

인터널 도구의 개시

위의 과정을 거치면서 새롭게 시작하는 글로벌 웹사이트 개편안에 적용하고자 하는 개선점들은 아래와 같이 정리되었어요.

  • 각 팀에서 public id를 생성하고 이를 받아오기보다, 우리가 콘텐츠의 id를 모두 갖고 있자.
  • 클라이언트는 우리가 처리하고, 각 팀의 서버 작업을 최소화하자.
  • 클라이언트 스키마를 정의하면, 따로 쿼리를 작성하지 않아도 해당 콘텐츠 모델 응답을 batchGet 하는 API를 생성해주자.
  • 각 팀의 서버를 직접 찌르지 않고, 중간에 persistence layer를 껴서 서버 응답을 기록하고 캐시하자.
  • SEO 관리를 위해 노출되어야 하는 콘텐츠를 선택할 수 있게 하자.

그렇게 인터널 도구인 Jampot을 만들게 돼요. 잼팟은 Jamstack에서 따온 ‘Jam’과 이를 모아놓는다는 의미의 ‘pot’의 합성어인데요. Gatsby를 사용해본 분들은 아시겠지만, Gatsby의 data layer과 비슷한 역할을 하는, 정의한 스키마에 따른 API를 모아놓은 레이어죠. 그렇게 데이터와 웹 어플리케이션 레이어를 명확하게 분리해, Headless CMS와 같이 각 팀의 서버에서 받아놓은 응답을 잼팟이라는 데이터 레이어에 쿼리로 모아놓고 잼팟 API를 사용해 페이지를 만들 수 있게 되었어요.

잼팟은 당근의 맥락이 전혀 담기지 않고 그 자체로 core 한 도구로 제작하여, 추후 오픈소스의 가능성을 열어두고자 했어요. 누구나 콘텐츠 모델을 생성하고 페이지 쿼리를 작성하면 렌더러에서 이를 사용할 수 있도록 persistence layer를 만들어주는 거죠. 이를 위해 graphql-tools를 사용하여 모델과 Query, Mutation을 생성할 수 있도록 서버 로직을 구현했어요. 또한 잼팟을 사용하는 유저는 불특정 다수로 가정했으므로 누구나 쉽게 만들 수 있는 만큼 관리하는 측에서 버그에 대한 대응이 잦을 수 있어요. 따라서 저희는 Event Driven Architecture 을 사용하여 생성, 수정, 삭제 등으로 이벤트를 정의하고 이를 모두 MongoDB에 쌓아 특정 이벤트를 타깃으로 롤백이 용이하도록 설계했어요. 또한 서버 응답을 캐시하기 위한 용도로 Redis를, 페이지를 캐시하기 위해 Cloudfront를 사용하여 모든 레이어에서 데이터를 캐싱하고 관리할 수 있도록 구성했답니다.

Jampot 대시보드
Jampot 유저를 위한 가이드

잼팟의 사용 방식에 대해 간단히 설명하면 다음과 같아요.

  1. 유저는 콘텐츠 모델을 생성한다.
  • 이 때, 콘텐츠 모델은 단일 콘텐츠와 복수 콘텐츠로 나뉜다. [참고]
  • 단일 콘텐츠의 경우 하나의 엔드포인트가 가리키는 데이터가 하나이므로 batchGet?ids= 로 id 에 맞는 데이터를 갖고 오거나, batchGet?size= 또는 batchGet?recent= 와 같이 특정 id 전달 없이 개수 또는 최신순으로 여러 데이터를 가져온다. e.g. 상세 페이지
  • 복수 콘텐츠의 경우 특정한 id의 데이터를 가져오는 것이 아닌 하나의 엔드포인트가 여러 데이터를 가져오는 경우이므로 하나의 엔드포인트를 전달한다. e.g. 홈 또는 검색 결과 페이지

2. 생성한 콘텐츠 모델을 사용하기 위한 페이지 쿼리를 작성한다.

  • 콘텐츠 모델 생성 시 기본적으로 잼팟에서 제공하는 쿼리는 단일/ 복수 콘텐츠에 따라 다르다.
  • 단일 콘텐츠는 하나의 쿼리로 하나의 데이터를 쿼리하므로 contentModel(id: String!) 과 같은 쿼리를 사용한다.
  • 복수 콘텐츠는 하나의 쿼리로 불특정한 여러 데이터를 쿼리하므로 contentModel 과 같이 id 등을 전달하지 않고 쿼리한다.

이 때 Jampot 엔드포인트를 사용하여 데이터를 fetch 할 수 있지만, 잼팟에 대한 정보를 모두 내장하고 있는 SDK를 통해 불필요한 코드를 모두 삭제하고 쉽게 필요한 데이터를 가져오도록 했어요. 따라서 데이터를 패치하고자 하는 국가코드(countryCode)와 사용하고자 하는 페이지 쿼리를 인자로 전달해 데이터를 패치하도록 했죠.

생성한 국가 리스트
생성한 페이지 쿼리 리스트

이를 통해 웹페이지는 정말 “정적” 으로 서빙되어 유저에게 빠른 속도로 정보를 제공할 수 있게 되었어요. 또한 서버 응답을 캐싱했기 때문에 각 팀에서 모니터링 시 이유를 모르는 latency가 발생하지 않아 불필요한 CS도 줄일 수 있었어요.

명확히 보이는 cache revalidate 시 batchGet request 수

유연하고 주도적인 SEO 관리

이렇게 여러 단계를 거쳐 웹사이트에 대한 오너십을 가져온 주된 이유는 바로 SEO로 귀결되어요. 당근을 웹에 많이 노출되게 하는 것이 저희의 가장 큰 목표였죠. 잼팟과 Remix를 사용하여 글로벌 웹사이트를 리뉴얼하고 배포한 뒤, 본격적으로 SEO 향상에 돌입했어요. 일단 국외에서 karrot 을 검색했을 때, 그리고 used sofa in Toronto 를 검색했을 때 상단에 노출되는 것을 목표로 여러 전략을 채택했어요. 이 과정에서 ahrefs라는 SEO 관리 툴을 사용하게 되는데, 정말 많은 유용한 정보가 들어있으니 비슷한 목표를 갖고 계시다면 한번 살펴보시는 걸 추천드려요.

ahrefs에 도메인을 등록하게 되면, 이 도메인에 걸린 키워드와 오가닉 트래픽, 백링크 등을 확인할 수 있어요.

ahrefs 대시보드로 보는 karrotmarket.com 지표

각각의 항목을 분석해 강점, 약점을 가진 키워드를 추출하고 트래픽이 오른 페이지와 기타 어떤 부족한 요소를 보완해야 하는지 분석해 전략을 수립할 수 있어요. SEO는 명시적으로 해야 하는 부분이 드러나 있는 분야가 아니라, 할 수 있는 최대한 사이트를 보완하면서 목적을 향해 여러 시도를 해보는 수 밖에 없어요. A/B테스트 같이, 어떤 시도를 했을 때 어떤 결과가 나왔는지 분석하고 또 다른 전략을 수립하는 식으로 꼬꼬무(꼬리에 꼬리를 무는..) 실험을 해야 했어요. 그래서 저희는 매주 한번씩 SEO 분석 회의를 잡고, 결과를 토대로 보완할 점을 찾아 태스크를 생성했답니다.

유저가 많이 검색해보는 키워드를 찾고, KD(Keyword Difficulty), Volume(the number of pages), Position을 분석해 목표하고자 하는 URL을 정리했어요. 또한, 페이지 내 참조하는 인터널 링크를 증가시키기 위해 사이트의 상호 참조 관계를 모두 생성할 수 있도록 했어요. 이 대부분의 작업들은 결국 사이트맵으로 정리돼요. 등록된 사이트맵을 검색봇이 들어가 포함된 모든 링크를 긁어간다고 보면 돼요. 여기에 영향을 미치는 외부 요인은 신뢰도인데, 이에 이 사이트를 참조하고 있는 링크가 얼마나 있는지를 나타내는 백링크 등이 중요하게 작용해요. 결국 karrotmarket.com/ 의 사이트맵을 정리하고 신뢰도를 높이고자 불필요한 링크를 정리, 리포트된 여러 에러를 제거하면서 글로벌 SEO 지표는 점차 향상되었어요.

리뉴얼 버전 배포 이전과 이후의 지표 변화 (1)
리뉴얼 버전 배포 이전과 이후의 지표 변화 (2)

또한 블랙박스인 부분을 조금이나마 밝히기 위해 어떤 검색봇이 얼마나 사이트를 크롤링해가는지 볼 수 있는 도표를 만들어, 갑자기 서버에 요청이 몰린 경우 원인을 봇과 기타로 분류하면서도 어떤 검색 포털에서 결과 향상을 기대할 수 있는지 확인하고자 했어요.

각 검색봇이 긁어간 비율 및 도표
매일 팀 채널에 오는 지표 알림

당근에 있어서 웹사이트의 역할이란?

당근에는 아직 정리되지 않은 웹사이트가 많이 퍼져 있어요. 단기적으로 나가는 마케팅성 페이지와, company-centric 콘텐츠를 담은 어바웃당근, 유저의 콘텐츠를 담은 당근닷컴, 그리고 서비스가 그대로 다른(서브) 도메인으로 분리되어 나가 있는 비즈니스당근, 당근페이 등의 사이트도 있어요. 서비스 별로 웹을 활용하고자 하는 이유와 방향은 모두 다를 수 있지만, 계속된 파편화로 인한 관리 부재를 막기 위해서는 웹사이트를 관리하는 주체가 필요해요. 이를 위해 잘 다듬어지고 관리되는 당근닷컴을 만들기 위해, 이 글을 작성하는 지금 국내 당근닷컴도 글로벌 웹사이트에 이어 리뉴얼이 진행 중이에요.

당신 근처의 당근

이번 마이그레이션이 완료되면, 당근 웹사이트는 당근 내 여러 서비스를 실험해볼 수 있는 playground로 작용할 수 있어요. 지금까지는 당근닷컴이 앱을 오가는 통로인 public web과 검색에 노출되는 SEO 부분을 주로 살펴봤지만, 웹을 필요로 하는 명확한 기능 외에도 앱에서 실험하기 어려운 여러 아이디어를 발현하고, 결과를 맛볼 수 있는 곳으로 사용될 수 있죠. 유저를 끌어오는 매개가 됨과 동시에 사내에서도 가볍지만 다양하게 사용되는 실험장이 될 수 있어요. 그래서 굉장히 많은 잠재력을 가지고 있고, 앞으로 많은 발전 가능성을 가진 플랫폼이라고 생각하고 있어요.

당근의 모든 것을 경험해보세요.

저는 프론트엔드코어팀에서 웹사이트를 개발하면서 당근의 모든 팀뿐만 아니라 회사를 관통하는 큰 문화를 경험할 수 있었어요. 각 목적 조직이 일하는 방식과 회사의 조직도, 운영되는 방식을 보며 당근이 장려하는 문화를 전체적으로 볼 수 있어 작은 그림부터 큰 그림까지 얕게라도 체험해 본 것 같아요. 스타트업처럼 린(lean)하게 일하면서 속도감과 쾌감을 경험해보기도, 팀과 회사의 방향을 얼라인하며 정책을 수립하는 동안 여러 이해관계를 경험하고 정리해보기도 했어요. 그 덕에 2년이란 짧고도 긴 시간 동안 압축적으로 당근이라는 한 회사에서 경험하고 성장할 수 있었어요.

퍼머링크 TF라는 단기적인 프로젝트에서 하나의 웹사이트 파트가 되기까지, 당근 웹사이트의 필요성을 꺼내고 디벨롭하는 기간 동안 해야 할 것을 스스로 정의하며 마치 제 사업을 키워나가듯 진심으로 일할 수 있었어요. 그 누구도 할 것을 정의해주지 않고, 스스로 본인의 프로젝트를 발굴하고 키워나가며 당근이라는 작은 사회에서 스타트업을 꾸리는 느낌으로 성장하고 싶은 분들에게 한치의 망설임 없이 추천드리고 싶어요. 지금 웹사이트 파트에서 채용 진행 중이니 이 글을 읽으며 마음이 설렌 분이 있다면 자유롭게 지원해주세요. 🙂

Software Engineer, Frontend — 프론트엔드코어 (웹사이트) | 당근 팀 채용

글을 마치며, 처음부터 함께 프로젝트를 발굴해 도움을 준 Tony, 큰 설계부터 중요한 부분까지 자문해준 Tim, 항상 차분하게 빈 부분을 메꿔주는 Levi, 그리고 웹사이트를 개발하기 위해 도움 주신 많은 분들 (중고거래실, 알바팀, 부동산팀, 중고차팀, 로컬프로필팀, 모바일플랫폼팀, 인프라실, 브랜딩팀 등) 에게 감사 인사를 전합니다.

--

--