오늘은 토스 자체 React Native 프레임워크 Bedrock가 어떻게 파일 기반 라우팅의 문제를 해결했는지 소개해드릴게요.
기술 블로그 모음
국내 IT 기업들의 기술 블로그 글을 한 곳에서 모아보세요

토스의 디자인 편집기 데우스(Deus)! 이번 모닥불에서는 토스의 자체 디자인 편집기 데우스(Deus) 프로젝트를 소개합니다! 디자인과 개발 사이의 경계를 넘나드는 새로운 가능성을 만나보세요! 데우스(Deus)가 풀어낸 기술적 도전과 그 여정을 통해, 우리가 함께 만들어갈 미래를 이야기합니다.

프로그래밍을 하다 보면 테스트의 필요성을 통감하는 순간이 찾아옵니다. 하지만 테스트를 꾸준히 작성하는 팀은 많지 않습니다. 만일 팀의 룰로 인해 강제로 테스트를 작성하고 있더라도, 업계의 구루가 말하는 TDD의 이점은 별로 와닿지 않고, 프로그램 코드보다 테스트 코드를 작성하는 데 더 많은 시간을 소모하고 있는 자신을 보며 자괴감을 느끼고 있을지도 모...

프론트엔드 개발 경험을 향상시키는 HMR(Hot Module Replacement)의 원리와 다양한 번들러가 HMR을 어떻게 지원하는지 살펴보고, ESBuild 기반 번들러를 직접 개발한 과정을 소개드려요.

왜 토스증권 PC의 그리드 레이아웃을 왜 직접 구현하게 되었는지, 그리고 어떻게 만들어져 있는지를 이제부터 소개해 드릴게요.

들어가며 안녕하세요. LINE+ UIT 조직에서 프런트엔드 개발을 하고 있는 강형민입니다. LY에서는 매년 'Front-end Global Workshop'을 개최하고 있습니다. ...

네이버 검색은 사용자 중심의 빠르고 원활한 검색 경험을 제공하기 위해 지속적으로 웹 성능을 모니터링하고 최적화하고 있습니다. 2024년에는 특히 LCP(Largest Contentful Paint)를 핵심 성능 지표로 삼아 여러 최적화 작업을 진행하였으며, 그 결과 네이버 검색의 웹 성능은 목표로 삼았던 구글 글로벌 LCP와 유사한 수준인 LCP p95 기준 2.31초를 달성할 수 있었습니다. 또한 2024년에 새로운 서치 피드 서비스가 도입되면서 기존의 웹 기반 성능 지표로는 측정하기 어려운 무한 스크롤 영역을 측정할 수 있는 새로운 성능 지표를 개발하여 관리하고 있습니다. 새로운 성능 지표는 동적 콘텐츠 로딩의 체감 속도와 이미지 렌더링 타이밍을 평가하는 데 활용됩니다. 이 글에서는 1) 2024년 네이버 통합검색의 웹 성능을 정리하고, 2) 새롭게 도입된 성능 지표를 소개하며, 3) 2024년 네이버에서 진행한 몇 가지 성능 개선 사례를 살펴봅니다. 2024년 네이버 통합검색 성능 연간 지표 네이버 검색은 LCP를 가장 중요한 성능 지표로 설정하고 있으며, 구글 글로벌 LCP 수준인 p95(95번째 퍼센타일) 기준 2.5초 이하를 목표로 하고 있습니다. 2024년 12월 기준 네이버 검색의 LCP Good Score는 96.59%를 기록했는데, 이는 대부분의 사용자들이 페이지 로딩 시 콘텐츠가 빠르고 원활하게 표시되고 있음을 의미합니다. 꾸준히 시스템을 모니터링하고 개선하기 위해 노력한 덕분에 2024년 하반기부터 성능 지표가 점진적으로 개선될 수 있었습니다. 2024년 네이버 통합검색 성능 개선 사례에서 구체적인 개선 사례를 확인할 수 있습니다. 2024년 네이버 검색의 전체 LCP는 p95 기준 약 2.31초이며, INP(Interaction to Next Paint)는 0.26초입니다. INP는 사용자가 버튼을 클릭하거나 입력을 했을 때 화면이 다음 단계로 넘어가기까지 걸리는 시간을 의미합니다. INP는 Core Web Vitals의 주요 지표 중 하나로 네이버 검색에서도 유심히 모니터링하고 있습니다. 구글에서는 좋은 INP를 200ms 이하로 안내하고 있습니다. 2024년 하반기부터 네이버 검색의 INP 지표가 점점 개선되고 있지만, 아직 목표 기준인 60ms를 초과하므로 추가적인 최적화 작업이 필요합니다. 2024년 네이버 통합검색의 새로운 성능 지표 네이버 검색은 최근 무한 스크롤 방식의 서치 피드(Search Feed)를 도입하여 사용자 경험을 개선하기 위해 노력하고 있습니다. 그런데 기존의 LCP 지표만으로는 동적으로 로드되는 무한 스크롤 영역의 성능을 정확하게 평가하는 데 한계가 있었습니다. LCP는 페이지를 최초로 로딩할 때 주요 콘텐츠의 시각적 완성도를 측정하는 데 효과적이지만, 사용자가 직접 스크롤로 새로운 데이터를 요청하는 서치 피드의 특성을 반영하기 어려웠습니다. 이러한 문제를 해결하기 위해 네이버 검색에서는 새로운 성능 지표를 정의하여 관리하고 있습니다. 피드 사용자 체감 성능 지표: FUPP FUPP(Feed User Perceived Performance)는 서치 피드의 동작 흐름과 사용자 상호작용을 기반으로 설계된 지표입니다. 사용자가 화면 아래까지 스크롤하여 새로운 콘텐츠를 요청하면, 백엔드 API 호출이 트리거되고 해당 데이터를 받아 화면에 렌더링하는 과정이 시작됩니다. FUPP는 API 호출이 시작되는 시점부터 서치 피드의 첫 번째 주요 이미지가 화면에 완전히 렌더링될 때까지의 시간을 측정합니다. 이는 단순히 네트워크 응답 속도뿐 아니라 데이터 처리와 시각적 요소의 표시 시간까지 포괄함으로써, 사용자가 실제로 새로운 콘텐츠를 '인지'하는 순간을 측정합니다. 즉, FUPP는 무한 스크롤 환경에서 사용자가 다음 콘텐츠를 얼마나 빠르게 확인할 수 있는지를 평가하는 기준입니다. 위 내용을 바탕으로 FUPP를 측정한 결과 p95 기준 약 1.5초로 나타났습니다. 이는 대다수 사용자가 1.5초 이내에 새로 로드된 서치 피드 콘텐츠를 확인할 수 있음을 의미합니다. 다시 말해 사용자가 화면 아래에 도달하기 1.5초 전에 서치 피드를 요청하면 끊김 없이 콘텐츠를 확인할 수 있습니다. 피드 이미지 로드 타이밍 지표: FILT 대부분의 서치 피드에서 가장 많은 영역을 차지하는 부분은 이미지입니다. 따라서 이미지 로딩 속도가 사용자 체감 성능에 많은 영향을 주게 됩니다. 네이버 검색에서는 FILT(Feed Image Load Timing)라는 지표를 도입해 서치 피드에서 이미지가 언제, 어떻게 로드되었는지를 분석하고 있습니다. 이미지가 노출되는 시점을 총 4가지 유형으로 세분화하여 FILT를 측정했습니다. 첫 번째 유형은 Standby(대기 중)로 이미지 로딩이 아직 시작되지 않은 상태를 의미합니다. 브라우저의 lazy loading 방식을 이용하므로 Standby는 아직 브라우저가 이미지를 로드하지 않은 상태입니다. 측정한 결과 전체 이미지 중 14%가 이 유형에 해당했습니다. 두 번째 유형인 Early(미리 로드됨)는 사용자가 해당 피드 영역에 도달하기 전에 이미지가 로드된 경우로, 사용자는 끊김 없이 피드를 소비할 수 있습니다. 전체 이미지의 75%를 차지합니다. 세 번째 유형은 Viewport(화면 내 로드됨)로 사용자가 피드에 진입한 시점에는 이미지가 로드 중이었지만 피드를 소비하는 과정에서 로드가 완료되는 경우입니다. 전체 이미지의 약 11%를 차지하고 있습니다. 마지막 유형은 Late(늦은 로딩)로 사용자가 피드를 지나친 후에 이미지가 로드된 경우입니다. 즉, 사용자가 해당 이미지를 확인하지 못한 상태로 피드를 지나쳤음을 의미합니다. 다행히 이런 경우는 0% 수준으로 확인되었습니다. 서치 피드 최적화 FUPP와 FILT 지표로 서치 피드의 성능을 정량적으로 측정하고 모니터링할 수 있는 체계를 구축했고, 이를 활용해서 서치 피드 최적화 실험을 진행했습니다. 무한 스크롤 방식의 고민 중 한 가지는 콘텐츠를 호출하는 시점입니다. 사용자가 화면을 끝까지 스크롤한 이후 콘텐츠를 로드한다면 대기 시간이 발생하여 사용성에 문제가 생길 수 있습니다. 반면 사용자가 화면 아래에 도달하기 전에 콘텐츠를 너무 빠르게 로드하면 소비 의도가 없는 사용자에게도 콘텐츠가 노출되어 서버 리소스가 불필요하게 소모될 수 있습니다. 그래서 우리는 로드 시점에 따른 사용성과 서버 부하 등을 확인할 수 있는 ABT를 진행했습니다. 화면 맨아래에서부터 200&Tilde1,600px 구간을 실험 구간으로 설정하고, 구간별로 서치 피드 호출 시점을 조정하여 소비 지표와 시스템 지표를 종합적으로 확인했습니다. 그 결과 콘텐츠 로드 시점을 100px 조정할 때마다 서버 호출량은 5%, 사용자 도달률은 2% 변동하는 패턴을 확인할 수 있었습니다. 예를 들어 화면 아래에서 600px 위치에서 콘텐츠를 로드하면, 800px 위치 대비 서버 호출량이 10% 감소했으나 사용자 도달률도 4% 하락했습니다. FILT 지표 역시 주목할 만한 변화를 보였습니다. 로드 시점이 늦어질수록 Viewport 비중이 증가했는데, 이는 이미지 요청 시점이 늦어졌기 때문에 발생한 현상으로 예측한 결과였습니다. 흥미로운 부분은 특정 시점(약 1,000px) 이후에는 로드 시점을 더 앞당겨도 Early 비중이 개선되지 않는다는 점이었습니다. 소비 지표(클릭률, 이탈률 등)에서도 유의미한 인사이트를 확인할 수 있었습니다. 로드 시점이 늦어질수록 소비 지표는 안 좋아질 것으로 예상했고, 실제로도 가설을 입증하는 데이터를 확인할 수 있었습니다. 반면 로드 시점이 빠를수록 소비 지표가 개선될 것이라는 초기 가설과 달리 특정 시점(약 1,200px)을 기준으로 소비 지표는 오히려 감소했습니다. 이는 콘텐츠 소비 의도가 없는 사용자에게까지 피드가 노출되므로 해당 사용자들이 피드를 무시하거나 빠르게 이탈하는 행동 패턴에 기인한 것으로 판단하였습니다. 실험 데이터를 종합한 결과, 화면 아래에서부터 600&Tilde1,000px 구간에서 서치 피드를 호출하는 것이 가장 효율적이라는 결론을 내렸습니다. 현재 네이버 검색은 1,000px을 기준점으로 채택하여 서비스 안정성과 사용자 경험을 동시에 개선했으며, 향후 트래픽 변동이나 사용자 패턴 변화에 따라 데이터 기반으로 유연하게 조정할 계획입니다. 2024년 네이버 통합검색 성능 개선 사례 네이버 검색에서 2024년에 진행한 몇 가지 성능 개선 사례를 소개합니다. 지역플러스 영역 LCP 개선 네이버 검색의 지역플러스 영역은 장소 검색 시 표시되는 검색 결과로, 주로 이미지와 함께 노출됩니다. 2024년 5월 30일 업데이트로 LCP p95 수치가 3,000ms에서 2,000ms로 30% 개선되었습니다. 이러한 성능 개선의 핵심 요인은 CSS의 background-image 속성을 사용해 이미지를 로딩하는 방식에서 img 태그를 사용하는 방식으로 변경한 데 있습니다. 과거에는 Internet Explorer 환경에 대응해야 해서 object-fit 속성 대신 background-image 속성을 사용할 수 밖에 없었습니다. background-image 속성을 사용하여 이미지를 삽입하는 방식은 브라우저가 DOM 파싱이 완료된 후에 CSS 파일을 파싱하면서 이미지를 요청합니다. 그 결과 이미지가 렌더링되기까지 추가적인 지연이 발생하며, 이는 LCP를 악화시키는 요인이 될 수 있습니다. 반면 HTML의 img 태그를 활용하는 방식은 HTML 문서를 파싱하는 즉시 img 태그를 인식하고 리소스 요청을 시작합니다. 이 방식은 브라우저의 초기 파싱 단계에서 이미지를 빠르게 불러오므로 이미지 렌더링 시간을 단축하고 LCP에 긍정적인 영향을 줄 수 있습니다. 다음의 그림은 각 이미지 로딩 방식을 적용했을 때 브라우저에서 이미지를 로드하는 과정을 나타낸 것입니다. 파란색 박스로 표시된 영역은 img 태그를 사용했을 때, 빨간색 박스로 표시된 영역은 background-image 속성을 사용했을 때입니다. 이번 개선 사례에서 네이버 검색의 성능 최적화 작업이 단순히 코드 최적화나 서버 인프라 개선에 국한되지 않으며, 프런트엔드 렌더링 전략의 미세 조정만으로도 상당한 효과를 얻을 수 있다는 점을 확인할 수 있었습니다. 네이버 통합검색 성능 개선에 도움을 주신 플레이스 검색 서비스 개발팀의 이대희 님, 임지수 님(메인&플레이스UI개발)께 감사드립니다. 크리스마스 브랜드검색 Flicking 성능 개선 크리스마스 시즌 동안, 네이버 검색의 특정 영역에서 egjs/Flicking을 활용한 UI의 화면이 끊기는 현상이 발생했습니다. 비록 이 문제가 Web Vitals 지표에는 큰 영향을 주지 않았지만, 사용자 경험에는 부정적인 인상을 남길 가능성이 높았습니다. 문제의 원인을 분석한 결과, 첫 번째로 marginLeft 속성을 지속적으로 변경함에 따라 브라우저가 레이아웃 재계산(reflow)을 빈번하게 수행하게 된 부분이 큰 영향을 주었습니다. 이로 인해 화면의 애니메이션이 부드럽게 이어지지 않고 중간에 끊기는 현상이 발생했습니다. 두 번째로, 배경 이미지를 CSS의 background-image 속성으로 로드했기 때문에 이미지 로딩 속도가 상대적으로 느려졌습니다. 이 문제를 해결하기 위해 marginLeft 속성 대신 transform:transX 속성을 적용하도록 안내했습니다. 이렇게 하면 레이아웃을 재계산하지 않고 GPU 가속을 활용하므로 애니메이션이 훨씬 부드럽게 구현됩니다. 또한 앞선 사례에서 소개한 것처럼 background-image 속성 대신 img 태그 기반의 방식으로 이미지를 렌더링하도록 변경하여 이미지 로드 속도를 향상시켰습니다. egjs/Flicking을 사용할 때 marginLeft를 사용한 원인을 확인해 보니 Flicking 애니메이션 도중 Parallax 효과를 구현하기 위해서였습니다. 사실 egjs/Flicking에서도 Parallax 기능을 제공하고 있었지만 당시 제공된 문서의 설명이 충분하지 않아 실제 적용 과정에서 어려움을 겪었던 것으로 파악됩니다. 이에 따라 관련 내용을 보완하였으며, 이외에도 egjs/Flicking에서 제공하는 다양한 기능을 제대로 활용할 수 있도록 지속적으로 문서를 개선할 계획입니다. 마치며 2024년 네이버 통합검색의 웹 성능은 꾸준한 관리로 지속적인 개선을 이루었으며, LCP p95 기준 2.31초를 기록할 만큼 안정적인 수준에 도달했습니다. 또한, 새로운 서치 피드 서비스에 적합한 성능 지표인 FUPP와 FILT를 도입하여 무한 스크롤 영역의 성능을 보다 정밀하게 측정하고 최적화할 수 있었습니다. 앞으로 다음과 같은 부분을 개선할 계획입니다. SSR(ServerSideRendering)과 부분 CSR(ClientSideRendering)이 공존할 때 성능 측정 방법 개발 INP 지표 개선을 위한 추가 최적화 네이버 검색은 지속적으로 성능을 관찰하고 개선하여 더욱 빠르고 효율적인 검색 환경을 제공하고 사용자 경험을 극대화하는 것을 목표로 하고 있습니다.

주요내용 25년 3월 소식에서는 다음과 같은 유용한 정보들을 만나보실 수 있습니다. 2024 Frameworks Year in Review Netlify의 웹덕후들이 2024년의 흐름 그리고 25년도에 대한 기대를 정리해 줍니다. How long is a second in JavaScript? JavaScript에서 '초'의 길이와 시간 측정의 역사와 과학적 배경을 탐구해 봅니다. Optimizing the Critical Rendering Path 웹 페이지의 초기 렌더링을 지연시키는 주요 자원들을 식별하고, 이를 최적화하여 사용자 경험을 향상시키는 방법을 다룹니다. Sunsetting Create React App React 공식 팀이 Create React App(CRA)을 더 이상 유지하지 않기로 결정했습니다. >> FE News 25년 3월 소식 보러가기 ◎ FE News란? 네이버 FE 엔지니어들이 엄선한 양질의 FE 및 주요한 기술 소식들을 큐레이션해 공유하는 것을 목표로 하며, 이를 통해 국내 개발자들에게 지식 공유에 대한 가치 인식과 성장에 도움을 주고자 하는 기술소식 공유 프로젝트 입니다. 매월 첫째 주 수요일, 월 1회 발행 되고 있으니 많은 관심 부탁드립니다. ▷ 구독하기

주요내용 25년 2월 소식에서는 다음과 같은 유용한 정보들을 만나보실 수 있습니다. Cascading Spy Sheets: Exploiting the Complexity of Modern CSS for Email and Browser Fingerprinting Javascript와 Cookie 없이 CSS만으로 브라우저 지문 채취의 가능함이 증명되었습니다 The Ai-Assisted Developer Workflow: Build Faster and Smarter Today AI를 활용한 다양한 개발 도구가 어떻게 개발 방식을 변화시킬 수 있는지 알아봅니다. Toss Frontend Fundamentals 토스의 프런트엔드 개발자들이 공개한 변경하기 쉬운 프론트엔드 코드를 위한 지침서 Do JavaScript frameworks still need portals? Dialog, popover, CSS anchor positioning 등의 기능으로 Portal(React), Teleport(Vue) 를 대체하는 방법을 소개합니다. >> FE News 25년 2월 소식 보러가기 ◎ FE News란? 네이버 FE 엔지니어들이 엄선한 양질의 FE 및 주요한 기술 소식들을 큐레이션해 공유하는 것을 목표로 하며, 이를 통해 국내 개발자들에게 지식 공유에 대한 가치 인식과 성장에 도움을 주고자 하는 기술소식 공유 프로젝트 입니다. 매월 첫째 주 수요일, 월 1회 발행 되고 있으니 많은 관심 부탁드립니다. ▷ 구독하기
모닥불 10화 특집: 캠프파이어 에피소드 🔥 이번 모닥불은 특별히 시청자 여러분과 함께하는 시간으로 준비했어요. 사전에 접수된 시청자 여러분의 다양한 사연과 질문 그리고 코드 리뷰까지! 지금 바로 확인해보세요!

배경2022년 리멤버 리브랜딩리멤버는 2022년 7월 ‘기회가 열린다, 리멤버’라는 슬로건과 함께 명함 앱을 넘어 비즈니스의 다양한 기회가 열리는 직장인 슈퍼앱으로 나아가고자 리브랜딩이 진행되었습니다.이 과정에서 리브랜딩 심볼의 큰 변화가 있었는데요. 명함을 연상시키는 기존 사각형을 벗어나 다양한 기회가 열리는 ‘문’을 형상화한 스퀘어로 진화했고, 이...
![[DAN 24] 데이터 기반으로 지속 성장이 가능한 네이버 검색 FE 시스템 구축하기](https://d2.naver.com/content/images/2025/01/01-2.png)
네이버 검색 FE 시스템 구축 과정에서 저희는 다음과 같은 문제에 직면했습니다. 첫째, 유사하지만 영역이 다른 경우 비슷한 작업을 반복하는 경우가 있었습니다. 검색의 각 영역은 마이크로서비스 아키텍처(MSA)로 이루어져 있으며 클라이언트 코드 역시 영역별로 관리되고 있었습니다. 이로 인해 영역 간 동일한 유형의 작업을 반복해야 했고, 경험이 축적되기보다는 업무의 성장이 저해되는 결과를 초래했습니다. 둘째, 유사한 UI를 매번 새로 개발해야 하는 비효율성이 존재했습니다. 코드의 재활용이 어려워서 유사한 패턴의 UI를 구현할 때마다 새로운 코드를 작성해야 했고, 이는 자원의 낭비로 이어졌습니다. 셋째, 데이터가 부족해 개선 작업이 어려웠습니다. 개선이 이루어져도 이를 측정할 수 있는 데이터가 부족해서 실제 효과를 파악하기 어려웠습니다. 그 결과, 어디부터 개선해야 할지, 문제는 없을지 등을 판단하기가 어려워 개선 작업의 방향성을 설정하기 어려웠습니다. 넷째, 피드백 주기가 지나치게 길다는 문제가 있었습니다. 디자인이 실제 코드로 구현되어 동작하기까지의 시간이 너무 오래 걸렸고, 이 과정 중에 디자인이 수정되면 재작업이 많아지는 비효율적인 상황이 발생했습니다. 이 글에서는 이 문제들을 해결하기 위한 저희의 해결 방안과 현재 고민하고 있는 문제를 공유하겠습니다. 해결 방향 이러한 문제를 해결하기 위해 저희가 생각한 해결 방향은 다음과 같습니다. 첫째, 서버 주도 UI(Server Driven UI) 방식을 도입했습니다. 비슷한 작업이 반복되는 문제를 해결하기 위해 UI를 한곳에 모으고 이를 재활용할 필요가 있었습니다. 서버에서는 비즈니스 로직만 구현하고 UI를 생성하는 서버를 별도로 두는 것이 적합하다고 판단하여 서버 주도 UI 방식을 채택했습니다. 이를 통해 클라이언트 측에서 UI를 반복해 개발할 필요 없이 서버에서 다양한 상황에 맞춰 유연하게 UI를 전달할 수 있게 되었습니다. 이는 비슷한 개발 작업을 줄이고 유지 보수의 효율성을 크게 향상시켰습니다. 둘째, 디자인 시스템(Design System)을 통한 해결책을 마련했습니다. 기존에는 UI가 유사해도 개발자가 달라서 별도로 개발하여 중복이 발생했다면, 이제는 자주 사용되는 UI 요소를 일관된 디자인 시스템으로 구축하여 재사용이 가능하도록 만들었습니다. 이는 개발자와 디자이너 모두에게 큰 도움이 되었으며, UI 개발 시간을 단축시키고 중복된 노력을 최소화할 수 있었습니다. 셋째, 데이터 기반의 접근 방식을 도입했습니다. 현재 데이터가 부족해 개선 작업이 어렵다는 문제를 해결하기 위해, 더 많은 데이터를 수집하고 이를 바탕으로 피드백 루프를 강화하는 계획을 수립했습니다. 특히 현재 사용되고 있는 UI를 분석하고 그 데이터를 수집하여, 이후 개발 및 운영 과정에 적극적으로 활용하고자 했습니다. 이를 통해 서비스에 많이 사용되는 컴포넌트와 모듈을 수집하고 사용자의 패턴을 파악하며 어떤 UI가 효과적인지에 대한 통찰을 얻어 UI와 UX를 최적화할 수 있게 되었습니다. 더 나아가, 데이터 분석 결과는 새로운 기능 개발뿐만 아니라 기존 UI의 개선 작업에도 큰 도움이 되었습니다. 마지막으로, Design To Code를 통해 제한된 환경에서의 자동화를 도입했습니다. 긴 피드백 주기를 줄이기 위해, 디자인을 코드로 변환하는 과정을 자동화하는 시스템을 적용하여 디자인과 개발의 간극을 줄였습니다. 이로 인해 디자이너가 수정한 내용을 신속하게 반영할 수 있게 되었고, 개발자는 재작업을 줄이며 효율성을 극대화할 수 있게 되었습니다. Flexible Rendering Engine 해결 방안의 첫 번째 주제인 유연한 렌더링 엔진(Flexible Rendering Engine), 서버 주도 UI와 디자인 시스템에 대해 설명하겠습니다. 서버 주도 UI(Server-Driven UI) 템플릿 및 컴포넌트 기반의 한계 2021년에 처음 도입한 템플릿 기반 방식은 미리 정의된 몇 가지 템플릿을 사용하여 신속하게 화면을 구성해 개발 속도를 향상시킬 수 있었습니다. 그러나 다양한 UI 요구 사항에 맞춰 동적으로 변화하기에는 한계가 있었습니다. 새로운 요구 사항이 발생할 때마다 템플릿을 수정하거나 새로 생성해야 했기 때문에, 마치 정해진 레고 블록으로만 조립해야 하는 것처럼 유연성이 부족했습니다. 그래서 저희는 컴포넌트 기반으로 전환했습니다. 컴포넌트 기반은 UI를 작은 단위로 분할하여 재사용이 가능했고 확장성 측면에서 이점이 있었습니다. 하지만 과도하게 세분화된 컴포넌트 구조로 인해 데이터 전송량이 크게 증가했고 서버와 클라이언트 간의 통신이 복잡해졌습니다. 그 결과, 컴포넌트 구조에서는 성능 저하와 복잡성 문제가 발생했습니다. 개발 방식 장점 단점 템플릿(블록) UI 표현이 적음데이터만 변경 생산성 확장성 컴포넌트 UI 표현이 많음데이터와 함께 조립 확장성 생산성 템플릿 방식과 컴포넌트 방식 모두 각기 다른 한계가 있었기에, 저희는 새로운 접근이 필요하다는 결론을 내렸습니다. Sweet Spot 찾기: 모듈 도입 템플릿 기반은 고정된 틀 안에서 신속하게 UI를 구성할 수 있었으나 확장성이 부족했고, 반면 컴포넌트 기반은 지나치게 많은 컴포넌트를 사용하면서 복잡성과 성능 저하 문제를 겪었습니다. 이러한 상황에서 저희는 이 두 가지 방식의 중간 지점, 즉 스위트 스폿(sweet spot)을 찾기 위해 모듈이라는 개념을 도입했습니다. 모듈은 템플릿보다는 유연하고, 컴포넌트보다는 덜 복잡한 단위입니다. 이를 통해 다양한 상황에서도 적절한 수준의 UI 조립이 가능하고 확장성과 성능을 모두 확보할 수 있다는 장점이 있습니다. 저희는 블록의 크기와 복잡도의 기준을 큰 틀에서는 정했지만 상세한 규칙은 만들지 않았습니다. 이는 서비스가 변화하면서 함께 변화하는 영역이라 저희가 제어하기 쉽지 않았고, 너무 제한된 상황을 만들 수 있었기 때문입니다. 대신 적절한 기준으로 모듈화를 하고, 이후 데이터를 기반으로 모듈을 설정하며, 데이터를 분석하면서 블록 수준을 조정해 최적의 구조를 찾아가는 방식을 선택했습니다. Fender(Flexible Rendering)의 구조 유연한 렌더링 엔진은 JSON 데이터를 통해 클라이언트의 UI를 동적으로 제어하는 서버 주도 UI의 서버입니다. 서버는 UI의 각 블록을 정의하여 클라이언트에 전송하고 클라이언트는 이 데이터를 받아 실시간으로 UI를 렌더링합니다. 이 구조의 중요한 특징은 모든 UI 요소를 자유롭게 변화시키는 것이 아니라, 적정한 수준에서 블록 단위로 제어한다는 점입니다. 이를 통해 저희는 복잡성을 줄이면서도 유연성을 확보할 수 있었습니다. 예를 들어, 서버가 특정 조건에 맞춰 JSON 데이터를 전송하면 클라이언트는 별도의 추가 개발 없이 즉시 UI를 변경할 수 있습니다. 유연한 렌더링의 주요 장점은 크게 세 가지입니다. 서버에서 UI를 제어하므로 유연하게 UI를 업데이트할 수 있다. 블록 단위로 제약을 두어 복잡성을 줄이고 성능을 최적화한다. 다양한 상황에서 블록을 재활용해 개발 효율성을 높일 수 있다. 높은 재사용성(High Portability) 이러한 방식으로 저희는 다양한 UI 요구 사항에 유연하게 대응하면서도 성능과 효율성을 모두 확보할 수 있었습니다. 대표적인 사례로 최근에 배포된 숏텐츠 서비스를 들 수 있습니다. 숏텐츠 서비스는 문서형 콘텐츠를 개인화된 키워드 방식으로 쉽게 보여주는 서비스로, 모바일, PC 등 여러 플랫폼의 검색, 메인 등 다양한 지면에서 활용되고 있습니다. 숏텐츠는 제공되는 각 지면마다 폰트 크기나 패딩, 버튼 등의 스타일이 조금씩 다르지만 대부분의 구조는 유사합니다. 각각 독립적으로 개발했을 때의 장점도 있지만, 다양한 지면에서 재사용함으로써 효율성을 증대시켰습니다. 디자인 시스템 레거시 시스템에 디자인 시스템을 도입하기 위한 전략 처음 디자인 시스템을 도입할 당시의 상황을 살펴보면, 이미 많은 곳에 디자인 시스템이 적용되어 다양한 사례가 있었지만 저희 팀은 경험이 없었습니다. 또한 Figma와 같은 도구가 편의성을 제공했지만 해당 도구에 대한 이해도가 높지 않은 상황이었습니다. 이미 성공한 사례는 많았기에 저희는 실패 사례에 초점을 두고 리서치를 진행했습니다. 대부분의 실패는 시스템을 만드는 것보다 적용하는 과정에서 발생했습니다. 디자인과 개발 사이의 간극으로 인한 이슈, 디자인 시스템을 만드는 사람과 사용하는 팀 사이의 한계가 주요 문제점으로 드러났습니다. 이러한 부분을 고려하여 저희는 디자인 시스템을 완벽하게 만드는 것보다 서비스에 디자인 시스템을 적용하는 것에 집중했습니다. 따라서 서비스 전체를 대표하는 디자인 시스템보다는, 당장 개발해야 하는 서비스를 기반으로 필요한 부분을 디자인 시스템으로 구축했습니다. 또한 개발자와 디자이너가 함께 협의하고 수준을 맞추기 위해 노력했으며, 적용 비용을 낮추기 위해 디자인 시스템을 만드는 사람과 사용하는 사람을 구분하지 않기로 했습니다. 디자인 시스템 구축 vs 서비스 개발: 무엇이 먼저인가 앞서 언급한 것처럼, 디자인 시스템을 도입할 때 저희는 디자인 시스템 구축과 서비스 개발의 우선순위에 대해 많은 고민을 했습니다. 디자인 시스템을 먼저 완성한 후 서비스에 적용하는 것이 이상적이겠지만, 당시 저희는 디자인 시스템 구축 경험이 부족했기에 디자인 시스템을 먼저 완성한 후에 적용하는 것은 비현실적이라는 결론에 도달했습니다. 그래서 저희는 서비스 개발 과정에 디자인 시스템을 점진적으로 도입하는 전략을 선택했습니다. 특히 통합검색 개편 프로젝트에서 새로운 영역을 대상으로 디자인 시스템을 적용해나갔는데, 이는 필요한 컴포넌트를 개발하고 디자인 시스템을 도입하는 데 적절한 테스트베드 역할을 했습니다. 또한, 초기에는 모든 컴포넌트를 직접 만들기보다는 외부 라이브러리인 Bootstrap을 활용해 빠르게 결과물을 확인하며 디자인 시스템의 가능성을 신속하게 검증했습니다. 그리고 점차 저희만의 컴포넌트를 추가하면서 시스템을 확장해나갔습니다. 디자인 시스템 구축 vs 디자인 시스템 사용: 함께 혹은 따로 디자인 시스템을 도입할 때, 저희는 초기 단계에서 시스템을 만드는 사람과 사용하는 사람을 구분하지 않고 동일한 팀에서 함께 일하는 방식을 택했습니다. 이는 디자인 시스템을 빠르게 적용하는 데 효과적이어서 초기에는 매우 유용했지만, 디자인 시스템의 품질을 향상시키는 데는 한계가 있었습니다. 그 이유는 개발자가 서비스 개발에 집중하면서 디자인 시스템 자체를 개선하기 어려웠기 때문입니다. 디자인 시스템의 문서화나 자체 완성도 측면에서 부족함이 발생했고 이는 생산성에도 영향을 미치기 시작했습니다. 이후 저희는 역할을 분리하는 전략을 도입했습니다. 디자인 시스템을 만드는 팀과 이를 사용하는 팀을 분리함으로써 시스템의 품질을 높이고 각 팀이 자기 역할에 집중할 수 있었습니다. 만드는 팀과 사용하는 팀이 같은 팀에서 일했던 경험이 있었기에, 역할을 분리한 이후에도 서로 잘 이해하고 원활하게 협업할 수 있었습니다. 이를 통해 시스템 적용 과정에서 큰 문제 없이 시스템의 품질을 지속적으로 향상시킬 수 있었습니다. 협업의 비용을 낮추기 위한 노력 협업 비용을 낮추기 위해 같이 일했고, 각 영역의 품질을 높이기 위해 분리했습니다. 협업이 원활했지만 협업 비용을 완전히 해소하기는 어려웠기에 프로세스를 더욱 체계화하여 생산성과 효율성을 높였습니다. 디자인 시스템에 없는 컴포넌트는 서비스에서 필요에 따라 빠르게 만들고, 이후 이를 디자인 시스템으로 이관. 비동기(async)적으로 동기화. 컴포넌트의 조합인 모듈은 디자인 시스템 모듈을 공통화하되, 서비스 개발 시 편의성을 위해 컴포넌트를 조합하여 템플릿으로 만든 후, 이후에 데이터를 바탕으로 비동기적으로 동기화. 즉, 디자인 시스템과 서비스를 정확하게 맞추기보다는 개발 과정에서 생산성으로 높이고 기술적으로 빈 공간을 업데이트하여 효율적인 구조를 만들기 위해 노력했습니다. 또한, 기존에 개발자와 디자이너가 수시로 함께 확인하고 피드백을 주고받으며 디자인 시스템을 구축해나가는 것은 효과적이었지만, 디자이너가 Figma를 업데이트할 때마다 개발자가 디자인 시스템에 반영하면서 많은 비용이 소요되었습니다. 이러한 문제를 해결하기 위해 Figma API를 활용해 주기적으로 업데이트 사항을 모아서 알림을 주는 도구를 개발했습니다. 이를 통해 반복되거나 불필요한 협업 포인트를 줄이고 변경 사항을 놓치지 않을 수 있게 되었습니다. 현재 해결하고 있는 고민 저희는 디자인 시스템을 성공적으로 도입하고 많은 문제를 해결했지만, 여전히 몇 가지 고민이 남아있습니다. 첫째, 새로운 디자이너나 개발자가 기존의 디자인 시스템에 적응하는 데 시간이 오래 걸린다는 문제가 있습니다. 현재는 디자이너와 개발자가 매우 긴밀하게 협업하고 있지만, 앞으로는 더 느슨한 협업 구조를 통해 효율성을 높이고 새로운 인력도 쉽게 적응할 수 있는 체계를 마련할 필요가 있습니다. 둘째, 특정 서비스에만 특화된 컴포넌트를 디자인 시스템에 포함시킬 필요가 있는지에 대한 고민이 있습니다. 모든 컴포넌트를 포함시키는 것이 효율적이지 않을 수 있으며, 특정 서비스에만 사용되는 컴포넌트는 별도로 관리하는 것이 더 나을 수 있습니다. 마지막으로, 시간이 지남에 따라 지속적으로 증가하는 컴포넌트를 어떻게 관리할 것인가가 중요한 과제가 되고 있습니다. 컴포넌트가 너무 많아지면 디자인 시스템 자체가 복잡해져 관리와 유지 보수가 어려워질 수 있습니다. 저희는 재사용 가능한 컴포넌트만을 엄선하고 불필요한 중복을 줄이는 방향으로 시스템을 지속적으로 개선해 나가야 합니다. 이런 고민을 저희가 어떻게 해결해 나가고 있는지 이어서 설명하겠습니다. Developer eXperience 지금까지도 많은 디자인과 개발이 이루어졌지만, 팀원이 늘어나면서 앞으로 디자인과 컴포넌트는 점점 증가할 것입니다. 기존 팀원도 모든 디자인과 컴포넌트를 기억할 수는 없을 것이고, 새로운 팀원도 이를 전부 파악해 두기 어려울 것입니다. 이러한 상황을 해결하기 위해 저희는 다음과 같은 질문을 해보았습니다. 우리가 만든 디자인과 컴포넌트는 무엇이 있을까? 어떻게 만들고 있을까? 어떻게 사용하고 있을까? 어떻게 관리할 수 있을까? 모두에게 동일한 경험을 어떻게 제공할 수 있을까? 이 장에서는 개발자 경험을 쌓고 공유하는 과정을 소개하겠습니다. 디자인 시스템을 만드는 사람과 사용하는 사람에게서 경험을 얻어 데이터로 변환하고, 그 데이터를 처음 접하는 사람 또는 모두에게 제공하면 자연스러운 순환이 이루어질 것입니다. 이렇게 되면 자연스럽게 순환하는 데이터를 통한 자동화로 이어질 수 있습니다. 경험을 공유하는 과정에서는 Webpack, Figma Plugin, Action 등 다양한 형식을 이용할 수 있습니다. 여기에서 얻은 데이터는 텍스트나 JSON과 같은 형식으로 최종 저장되며, 순환하여 더 나은 시스템을 만드는 데 도움을 줄 수 있습니다. META: 경험을 데이터로 만들고 공유하기 자동화를 제공하거나 반복적인 작업을 줄이는 등 편의성을 제공하거나 시스템을 만들기 위해서도 데이터와 정보가 매우 중요합니다. 저희의 경험을 데이터로 나타낼 수 있는 첫 시작인 META를 소개하겠습니다. 여기서 말하는 META는 프로젝트에 있는 모든 정보를 의미하며, CSS 파일, 데모, JSDoc, 템플릿, 컴포넌트, 모듈, 타입 등이 있습니다. 이 META를 통해 경험을 공유하려고 합니다. 저희가 만든 META Analyzer를 통해서, 프로젝트에 담긴 코드와 파일로부터 META를 만들 수 있습니다. META Analyzer는 AST(Abstract Syntax Tree, 추상 구문 트리) 파서를 이용해 만들었습니다. AST는 소스 코드의 문법을 트리 형태로 표현한 구조로, 컴파일러에서 많이 사용합니다. export, let, const, boolean, number 등 저희가 알고 있는 문법 하나하나가 트리 구조의 노드에 해당합니다. 잘 알려진 AST 파서로는 Babel, TypeScript가 있으며 번들러, 트랜스파일러, 프레임워크, 문서 등에서 사용되고 있습니다. META에는 TypeScript 모듈의 AST 파서를 사용했습니다. index, entry에서 시작하여 연결된 모든 파일의 구조를 파악할 수 있습니다. 코드를 파싱하면 다음과 같이 export, function, import, JSX 등 어떤 문법을 사용했는지 알 수 있습니다. AST를 이용하면 문법 외에 어떤 정보를 얻을 수 있을까요? Visual Studio Code의 툴팁에서 인터페이스가 사용된 위치와 정의된 위치를 확인할 수 있듯이, 단순히 인터페이스나 컴포넌트의 유무뿐만 아니라 다음과 같은 연관 관계 정보를 알 수 있습니다. 컴포넌트를 사용했을 때 어떤 모듈에서 가져왔는지 스타일을 사용했을 때 어떤 CSS를 가져왔는지 인터페이스를 사용했을 때 정의된 위치가 어디인지 컴포넌트에는 Props를 사용하고 있고, Props에는 JSDoc, 상위 집합(superset), 상속, 의존 관계 등이 포함됩니다. 이를 통해 컴포넌트와 템플릿을 기준으로 시작하여 연관된 모든 정보를 가져올 수 있습니다. META 활용 사례 컴포넌트와 템플릿, 그리고 연계된 정보는 곧 경험입니다. 저희는 META를 통해 이 경험을 공유할 수 있습니다. 그런데 META로 경험을 공유할 수 있다면 정확히 어떤 것이 가능해질까요? 정보를 어떻게 다룰지는 정보를 가지고 있는 사람에게 달려있습니다. 정보를 어떻게 다루고 경험을 어떻게 표현하고 공유할 수 있는지, META를 활용한 여러 사례를 소개하겠습니다. 디자인과 개발의 1:1 매칭: Figma 플러그인 API를 활용한 Design to Code 첫 번째 사례는 디자인과 개발의 매칭입니다. 개발자는 디자인 요소를 개발로 옮겨야 합니다. 어떤 디자인이 어떤 컴포넌트로 연결되는지 모든 개발자가 알기는 어렵고 새로 입사한 팀원은 만들기도 어려울 수 있습니다. 디자인 요소를 개발로 옮기는 방법을 몇 가지 소개하면 다음과 같습니다. 디자인을 재사용하기 위해 컴포넌트화하고, 컴포넌트화된 디자인에 대해 개발 컴포넌트로서 함수를 정의하고 마크업을 작성하여 개발 디자인의 variants 타입에 따라 디자인의 모양과 값을 확인할 수 있으며, 스타일에 hex 값 또는 variable을 사용 가능 개발 시에는 조건에 따라 Props로 동기화해서 개발하고, 스타일에 CSS variable을 사용해 디자인과 개발 매칭 이와 같이 디자인과 개발 요소가 연결되므로 어떤 디자인이 어떤 컴포넌트와 동기화되어 있는지 파악해야 합니다. 개발 시 디자인의 요소 스타일, 변수, variants 등을 고려해야 하며, 디자인을 통해 마크업, CSS, SVG까지 개발해야 할 수도 있습니다. 여기서 다음과 같은 질문이 제기됩니다. 디자인을 통해 마크업을 쉽게 할 수 있을까? HTML 대신 다른 형식으로 바꿀 수 있을까? 이렇게 만든 디자인과 컴포넌트를 전부 기억할 수 있을까? 디자인 컴포넌트처럼 개발 컴포넌트도 재사용할 수 있을까? 모든 개발자가 처음부터 디자인을 잘 이해하는 것은 아닙니다. 스타일, 변수, variants 등 디자인 요소를 이해하기 어려울 수 있고, 마크업 작업 경험이 적다면 더욱 어려울 수 있습니다. 그렇기 때문에 디자인을 쉽게 이해하고 접근하기 위해 디자인을 코드로 바꿔주는 도구가 필요했습니다. 하지만 에디터로 개발하기에는 목적, 대상, 범용성, 기능이 매우 제한적이고 개발 비용과 유지 보수 비용이 매우 클 것이라고 판단해 조건을 변경했습니다. Design To Code라는 개념은 그대로 유지하되 에디터가 아닌 단순 플러그인으로 한정된 기능만 제공하여, 한 번의 클릭으로 하나의 결과를 얻어낼 수 있게 했습니다. 범용성은 적더라도 목적과 대상이 한정되어 유지 보수도 용이하다고 판단했습니다(이 시점은 Figma의 Variable과 Code Connect가 출시되기 이전입니다). Figma 플러그인 API를 이용하면 Figma 디자인에 접근하여 디자인 정보, 파일 정보, 프로젝트 정보를 파악할 수 있으며, 이 정보를 이용해 디자인을 개발로 변환할 수 있습니다. Figma 플러그인은 크게 API 레이어와 UI 레이어로 나뉩니다. API 레이어: Figma 플러그인 API를 통해 디자인 정보를 확인 UI 레이어: UI를 표현할 수 있지만 디자인 정보를 확인할 수 없음 따라서 API 레이어에서 얻은 디자인 정보를 UI 레이어로 전송해야 합니다. API 레이어에서는 플러그인 API를 통해 얻은 정보와 메서드를 호출해 얻은 정보에서 불필요한 정보를 제거하고 이름, 사이즈, 위치, 스타일, 키 등 필요한 정보만으로 정제된 데이터를 생성해 UI 레이어로 전송합니다. UI 레이어는 옵션에 따라 데이터를 JSX(JavaScript XML), JSON 등의 형식으로 변환하여 사용자에게 보여줍니다. 프로토타이핑한 플러그인은 다음과 같이 간단한 UI로 구성되어 있습니다. 왼쪽에는 가져온 코드를 보여주고 오른쪽에는 코드를 그대로 화면으로 렌더링한 결과를 보여줍니다. 플러그인으로 마크업을 대체할 수 있으므로 세부 작업만 추가하고 개발로 옮겨 시간을 크게 절약할 수 있게 되었습니다. 다만, 이 과정은 디자인 정보를 통해 마크업을 새로 만드는 과정이기에 새로 만드는 경우에 효과적입니다. 하지만 이전에 만든 컴포넌트가 존재하는 경우에는 어떨까요? 이미 존재한다는 사실을 모르고 중복 개발하면 낭비가 될 수 있습니다. 디자인이 어떤 컴포넌트와 연결되는지 알려주고 비슷한 디자인을 매칭해 준다면 이를 방지할 수 있을 것입니다. 플러그인에서 디자인 정보를 토대로 기존의 컴포넌트와 매칭하고, 매칭되지 않으면 앞에서 설명한 방식으로 새로 개발하면 됩니다. 기존에 만든 컴포넌트를 어떻게 플러그인에 연결할까요? 플러그인 API로 얻은 디자인 정보를 컴포넌트의 Props 인터페이스에 JSDoc 주석 형태로 주입합니다. JSDoc 주석을 선택한 이유는 개발과 다른 도구에 영향을 끼치지 않기 때문입니다. 이를 위해 커스텀 태그를 사용해 Figma 정보를 주입합니다. 예를 들어 컴포넌트 집합체에서는 set key를 사용하고, 각 컴포넌트에는 key를 사용하고, 패턴이 있는 이름의 경우에는 이름을 사용해서 디자인과 컴포넌트를 매칭합니다. 컴포넌트 매칭과 유사하게 Figma의 구조나 variant의 타입에 따른 값을 Prop의 JSDoc으로 주입함으로써 디자인과 개발을 연결할 수 있습니다. variant의 타입이 instance, boolean, text인 경우 값을 주입할 수 있으며 그 밖의 커스터마이즈한 값을 직접 수정할 수 있습니다. 이러한 정보는 플러그인 API를 통해서만 얻을 수 있기 때문에 일반적으로 얻기는 어렵지만, 플러그인에서 개발하는 경우에는 매칭에 필요한 정보가 JSDoc에 담겨 제공됩니다. 이러한 JSDoc 정보는 인터페이스에 담겨있고, 인터페이스는 META에 포함됩니다. META가 저장된 위치 URL을 플러그인에서 추가하여 사용할 수 있습니다. 저희가 만든 Search Design System과 디자인 시스템을 사용 중인 Fender도 META를 사용하고 있어서, 두 META를 플러그인에서 추가하여 사용할 수 있고 외부의 컴포넌트도 적합하다면 META로 추가하고 사용할 수 있습니다. 디자인 정보가 담긴 첫 데이터는 아무것도 매칭되지 않은 원시(RAW) 데이터입니다. META에 담긴 컴포넌트 정보와 디자인 정보가 일치하면 원시 데이터는 매칭된 컴포넌트 데이터로 변환됩니다. 매칭된 데이터는 매칭되기 전 데이터와 인터페이스가 동일하기 때문에 이전과 동일하게 JSX나 JSON 또는 그 밖의 다양한 형식으로 출력할 수 있습니다. META에는 Props 정보, 컴포넌트 정보, 연계된 스타일 정보도 포함되어 있습니다. 매칭되는 경우 기존 컴포넌트로 대체하여 스타일을 생략하고, Props 연결, 문서 연결 등 개발자에게 다양한 도움을 제공할 수 있습니다. 또한 Fender의 경우 JSON 형식으로 변환이 가능하며, Fender JSON Simulator에서 복사해 붙여넣어서 동일하게 출력할 수 있습니다. 디자인과 개발을 연결하는 플러그인(Design to Code) 적용 전후의 효과를 요약하면 다음과 같습니다. 플러그인 적용 전 플러그인 적용 후 디자인이 어떤 컴포넌트와 연결되었는지 직접 확인해야 함 연결되는 컴포넌트가 존재하지 않는다면 직접 마크업하여 컴포넌트를 개발해야 함 디자인에 해당하는 컴포넌트를 직접 찾을 필요 없이 플러그인에서 매칭 매칭되지 않는다면 새로운 컴포넌트로서 원시 마크업으로 제공됨 새로운 컴포넌트 개발과 기존 컴포넌트 재사용에 있어 더 나은 편의성을 제공하여 마크업 및 JSON 변환 비용을 절감 디자인 차이점 비교(Design Diff) 두 번째 사례는 디자인 차이점 비교(Design Diff)입니다. 개발자는 디자인을 따라 컴포넌트를 개발합니다. 플러그인을 통해 만든 컴포넌트는 하루가 지나도 잘 매칭이 되고 있을까요? 대부분은 매칭이 잘 되고 있을 것입니다. 하지만 매칭이 되지 않거나 디자인이 바뀌었다면 개발자가 알기 어렵습니다. Figma의 웹훅과 람다를 이용하여 변경 알림을 개발해보았지만, UI가 실제로 바뀌지 않아도 1분마다 알림이 왔고 변경 사항 확인도 불가능했습니다. 그래서 웹훅을 사용하는 대신 직접 차이점을 알아내려고 했습니다. 플러그인을 통해 만든 컴포넌트라면 META에 등록 및 관리되고 있다고 볼 수 있습니다. META에는 컴포넌트마다 Figma 디자인 정보인 key를 포함하고 있습니다. 또한 Figma REST API에서는 node-id나 key를 통해 스냅샷 이미지를 생성할 수 있습니다. META에 등록된 모든 컴포넌트의 스냅샷 이미지를 저장하고, 일정 주기마다 새로운 스냅샷과 이미지를 비교해 변화를 감지합니다. 또한 다음과 같은 변경 사항도 감지할 수 있습니다. 이름이 변경된 경우 key 값 변경으로 인한 매칭 해제 잘라내기로 인한 id, key 값 변경 이러한 변경 사항에 대한 알림을 메신저로 전달합니다. 이를 통해 변경 사항을 바로 알 수 있으므로 오류를 방지할 수 있었고 커뮤니케이션 비용이 감소했습니다. 트리거는 다음과 같은 경우에 발생합니다. 개발 코드가 머지되는 경우 디자이너/개발자의 활동 시간에 약 4번 정도 검사 스케줄 실행 실제 적용 사례를 보면, Q 아이콘의 변경에 따른 디자인 변화가 있었을 때 알림이 발생하여, 디자이너가 놓칠 수 있었던 변경 사항을 바로 확인하고 개발에 반영할 수 있었습니다. 디자인 차이점 비교(Design Diff) 적용 전후를 살펴보면 다음과 같습니다. Design Diff 적용 전 Design Diff 적용 후 디자인 변경 시 다음 회의에서 변경 사항을 공유받아 개발 변경 사항 확인을 놓쳐서 QA 단계에서 이슈 발생 변경 사항에 따른 알림이 오면 사유를 확인하여 즉시 개발 반영 및 공유 가능 지금까지 디자인 변경 2건, 개발 변경 1건을 바로 확인하여 오류 방지 통계 분석(Analytics): 템플릿과 컴포넌트 사용 현황 확인 세 번째 사례는 사용처를 확인하는 통계 분석(Analytics)입니다. 저희가 만든 컴포넌트와 템플릿이 어디에서 사용되고 있는지, 디자인은 어디에서 사용되고 있는지, 그리고 반대로 사용되지 않는 컴포넌트, 템플릿, 디자인이 있는지 확인이 필요했습니다. 정적으로는 코드 빌드를 통해 얻은 META에서 연관 관계를 파악하여 자주 사용되거나 사용되지 않는 컴포넌트, 템플릿을 대략 확인할 수 있습니다. 하지만 데모가 변경되어 실제로는 사용되지 않을 수도 있기 때문에, 실시간으로 로그에 컴포넌트와 템플릿 사용 기록이 담겨 전송된다면 더욱 정확하게 사용 현황을 분석할 수 있습니다. 또한 META와 연동하여 전체 템플릿, 컴포넌트 중 가장 많이 사용된 것은 무엇인지, 미사용 템플릿 현황은 어떤지, 해당 미사용 템플릿이 적용된 서비스가 종료되었는지 혹은 출시 전인지 확인하여 정리 대상 여부를 판단할 수 있습니다. Fender와 SDS(Search Design System) META를 통해 템플릿의 실제 사용처를 확인하는 통계 페이지에서 확인할 수 있는 정보는 다음과 같습니다. 템플릿과 컴포넌트의 관계 사용 여부 SDS 컴포넌트를 얼마나 잘 사용하고 있는지 여부 미사용 여부와 기간 등 문서(Docs) 네 번째 사례는 문서입니다. 문서에서는 프로젝트에 어떤 컴포넌트가 있는지, 컴포넌트의 역할, 모습, 사용 방법 등을 확인할 수 있습니다. 여기서 META를 이용하면 인터페이스 prop, type 등에 해당하는 기본적인 정보와 JSDoc 정보를 통해 클래스 이름, 설명, Figma key 등을 확인할 수 있어, 예시와 연결하여 control, 인자 타입, 설명에 도움을 줄 수 있습니다. 이외에도 여러 도구에서 META를 활용할 수 있습니다: Visual Studio Code와 같은 IDE(Integrated Development Environment, 통합 개발 환경) 도구 배포된 컴포넌트에서 JSDoc이 자동으로 툴팁을 제공해 문서 역할을 수행하며 개발 시 즉각적인 도움말 제공 Storybook META를 이용해 타이틀 영역을 자동으로 생성 Figma Key를 이용해 컴포넌트와 연결된 디자인 링크를 추가 Figma 플러그인 디자인과 연결된 컴포넌트 확인 사용 예시를 바로 확인 가능 또한 META를 통해 어떤 Prop이 ReactElement로 사용되었는지에 따라 반자동으로 디자인 구조인 Anatomy를 확인할 수 있습니다. Prop이 어디에 반영되는지, 사이즈와 사용되고 있는 클래스까지 확인할 수 있습니다. 최소한의 작업으로 META에서 사용자에게 필요한 정보를 추출하여 보여줄 수 있고, 문서와 플러그인의 연결, 문서와 디자인의 연결을 통해 언제 어디서든 사용 방법에 대한 도움을 제공할 수 있습니다. 네이버 검색 FE 시스템 아키텍처 디자이너는 디자인 시스템을 설계합니다. 이렇게 설계된 디자인 시스템은 Search Design System 플러그인을 통해 컴포넌트로 전환됩니다. 디자인 시스템을 기반으로 서비스 디자인을 설계하며, 물론 디자인 시스템에 없는 디자인도 존재할 수 있습니다. 기존의 디자인 시스템 컴포넌트는 Search Design System 플러그인을 통해 매칭되고 새로운 컴포넌트는 새로 개발합니다. 개발한 결과는 Fender를 통해 서비스됩니다. 서비스로 출시된 이후에는 사용자의 피드백과 META를 통해 얻은 정보를 바탕으로 개선할 부분, 반복되는 작업 등을 파악합니다. 이 정보는 다시 디자인 시스템과 서비스에 반영됩니다. 이러한 순환 구조를 통해 데이터를 기반으로 지속적으로 성장 가능한 FE 시스템을 구축하고 있습니다. 남아있는 고민 디자인 토큰을 설계하고 Figma와 연동하는 방법, 디자이너와 개발자, 디자인 시스템을 만드는 팀원과 사용하는 팀원 사이의 워크플로우, 서비스를 배포하고 오류를 수집해서 개선하는 DevOps 등 많은 주제가 있지만, 지면 관계상 여기서는 다루지 않겠습니다. 여전히 고민은 남아있습니다. 새로운 팀원이 왔을 때 이런 프로세스에 잘 적응할 수 있을지 현재 UI 재활용성이 사후에 많이 개선되는데, 사전에 재활용성을 높일 수 있는 방법은 없을지 UI 재활용만큼 인터랙션도 재활용할 수 없을지 전보다 많은 데이터를 볼 수 있게 되었는데 이를 어떻게 더 잘 활용할 수 있을지 이러한 고민은 지속적으로 해결해나가고 그 과정을 다른 기회에 공유하겠습니다. 이 글은 TEAM NAVER CONFERENCE 'DAN 24'에서 발표한 내용을 토대로 작성되었으며, 발표 내용과 자료는 DAN 24에서 보실 수 있습니다.
모닥불 10화 특집: 캠프파이어 에피소드 🔥 이번 모닥불은 특별히 시청자 여러분과 함께하는 시간으로 준비했어요. 사전에 접수된 시청자 여러분의 다양한 사연과 질문 그리고 코드 리뷰까지! 지금 바로 확인해보세요!

안녕하세요. LINE Plus ABC Studio 기획자 한영주입니다. 저는 일본 최대 규모의 배달 서비스인 데마에칸(Demaecan, 出前館) 앱을 기획하고 있습니다. 한국의 배...
![[프로모션] 마켓플레이스 웹 기반 데이터 분석 솔루션 Beusable](https://blogthumb.pstatic.net/MjAyNTAxMjRfOTMg/MDAxNzM3Njk2MjgzNTQ1.mBTc4bXA7oAXA_yy3jVGnRJU1R5stxyD387KhFcRm5Ig.SPpYtdriJKsTKPVAo86OeJHaugoVykRhPTEgW2PvvlEg.PNG/241201_뷰저블민간.png?type=s3)
안녕하세요. 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. 고객 데이터를 분석하고 싶은데 어떻게 해야 하지? 쉽게 홈페이지의 개선 포인트를 찾을 수 있을까? 홈페이지의 개선 포인트를 찾고 싶은데 어디서부터 시작해야 할지 막막하고, 데이터 분석부터 시작하려고 하니 어려운 외산 툴을 익히느라 힘들었던 경험을 했던 분들이 많이 있...
![[GS리테일] DX본부 편의점DX팀/O4O DX팀 Front-End/Back-End/모바일앱 개발 경력사원 채용(~02/02)](https://blog.kakaocdn.net/dn/lP1Ph/btsLVwSmvvb/ATn7Yvh9VEl7RkK0ViJIe1/img.png)

안녕하세요. LINE Plus ABC Studio에서 일본 음식 배달 서비스 Demaecan(出前館, 이하 데마에칸)의 디자인을 담당하고 있고, 사용자의 다양한 목소리를 담을 수 ...
유종의 미 그리고 회고
Kotlin Multiplatform은 에코시스템의 라이브러리 수가 2024년에만 35% 늘어나며 빠르게 성장하고 있습니다. 그러나 라이브러리 수가 그 어느 때보다 많아졌음에도 사용 사례와 타깃 플랫폼에 맞는 라이브러리를 찾기는 여전히 쉽지 않습니다. iOS와 Android에서 권한을 처리할 솔루션을 찾는 경우도, 모바일과 웹을 모두 지원하는 Comp...

Compose Multiplatform은 개발자가 다양한 플랫폼에서 UI 구현을 공유할 수 있도록 JetBrains에서 설계한 선언적 UI 프레임워크입니다. 1.7.0 릴리스는 공통 코드의 구성 요소 추가, 타입에 안전한 탐색 지원, iOS에서의 큰 성능 개선을 제공하고 최신 Jetpack Compose 업데이트의 변경 사항을 포함합니다. 이번 릴리스...

안녕하세요. 드라마앤컴퍼니에서 현재 채용 서비스를 개발하고 있는 웹 프론트엔드 개발자 오종택입니다. 이전에는 동료 분들의 비즈니스 임팩트를 극대화 하기 위한 UTS(User Targeting System, 조건에 맞는 유저를 찾아주는 쿼리 빌더) 등의 인터널 제품을 만들기도 했습니다.리멤버 웹 팀은 리멤버 블랙, 리멤버 채용 솔루션 등 모든 서비스의 ...

주요내용 25년 1월 소식에서는 다음과 같은 유용한 정보들을 만나보실 수 있습니다. Web Almanac 2024 전 세계 1천7백만개의 웹사이트를 분석, 현재 웹 기술의 사용과 웹페이지들의 상태를 확인할 수 있습니다. JavaScript Benchmarking Is a Mess JS 벤치마크 수행결과는 얼만큼 신뢰할 수 있을까요? 어떤 요소들이 결과에 영향을 미치고 있을까요? How Big Technical Changes Happen at Slack Slack은 기술적 변화를 어떻게 관리하고 기술 도입을 위한 전략을 확인해 보세요. Anders Hejlsberg: How we wrote the TypeScript compiler TS 코어 개발자로부터 들어보는 TS 컴파일러 개발 이야기 >> FE News 25년 1월 소식 보러가기 ◎ FE News란? 네이버 FE 엔지니어들이 엄선한 양질의 FE 및 주요한 기술 소식들을 큐레이션해 공유하는 것을 목표로 하며, 이를 통해 국내 개발자들에게 지식 공유에 대한 가치 인식과 성장에 도움을 주고자 하는 기술소식 공유 프로젝트 입니다. 매월 첫째 주 수요일, 월 1회 발행 되고 있으니 많은 관심 부탁드립니다. ▷ 구독하기

- ViewModel에서 발생하는 Event를 전파하기 위해 SharedFlow를 활용한 EventFlow로 사용하고 계신가요?- EventFlow 개념을 제일 처음 만든건 헤이딜러 였는데요. 헤이딜러에서도 이제 EventFlow를 사용하지 않습니다.- 어떤 코드로 개선했는지 내용을 확인해보세요안녕하세요헤이딜러 안드로이드팀 박상권입니다.여러분은 안드로...

안녕하세요. 저는 로컬 비즈니스실에서 서버 개발자로 일하고 있는 에렌(Eren)이라고 해요. 당근을 사용해 본 적 있으신가요? 당근의 동네지도 탭에 들어가면 ‘음식점’, ‘카페/간식’ 등 카테고리별로 동네 업체를 모아볼 수 있는데요. 여기서 각 업체의 정보를 담고 있는 ‘비즈 프로필’을 저희 팀이 만들고 있어요. 저희 팀은 사람들은 믿을 만한 동네 업체를 빠르게 찾고, 동네 사장님들은 더 효과적으로 장사할 수 있도록 돕는 플랫폼을 만들고 있어요.로컬 비즈니스실의 핵심 모델은 Business Profile(업체 프로필)이에요. 다양한 카테고리의 업체 사장님들이 비즈 프로필을 효과적으로 사용하기 위해선 여러 도구가 필요해요. 그래서 저희는 상품 판매, 예약, 후기, 쿠폰, 채팅, CRM 같은 여러 기능을 계속 추가하고 있어요. 동네 사장님의 필수 채널이 되는 것을 꿈꾸는 만큼 제품의 복잡도가 빠르게 증가하고 있어요.로컬 비즈니스실은 각 도메인의 복잡한 정책을 나누기 위해 마이크로서비스 아키텍처를 지향하고 있어요. 그럼에도 도메인 자체가 상호작용할 필요가 늘어났고, 각 서비스의 데이터를 조합하는 일도 증가했어요. 특히 다음과 같은 상황에서 복잡한 데이터 조합이 필요했어요. 대표적으론 다음과 같은 경우가 있었어요.모든 정책과 연관 관계가 모이게 되는 프로필 홈 화면.정책 자체가 여러 도메인의 데이터를 기반으로 결정되는 경우.외부 서비스에 데이터를 취합하여 데이터를 제공하는 경우.서비스가 고도화되면서 도메인 간의 관계는 점점 복잡해졌어요. 트래픽 또한 1만 TPS를 뛰어넘게 되었고요. 조회해야 할 데이터 관계는 늘어나고 시스템의 부하도 증가했어요. 로컬 비즈니스실에서 조회 로직과 관련해 구체적으로 어떤 문제가 발생했고, 어떻게 해결했는지 공유하고자 글을 작성하게 되었어요. 비슷한 고민을 하고 계시는 다른 분들에게 도움이 되면 좋겠어요.로컬 비즈니스실이 마주한 문제 — Dynamic Join Aggregator로컬 비즈니스실은 객체 간의 연관 관계를 조합하기 위해 Aggregator 패턴을 사용하고 있었어요. Aggregator 패턴은 간단하게 구현하기 좋다는 장점을 가지고 있어요. 예를 들어 프로필과 프로필의 카테고리 정보가 같이 필요하다면 다음과 같은 코드를 작성할 수 있어요.// 로컬프로필 조회val businessProfiles = businessProfileRepository.findAllBy(businessProfileIds)// 카테고리 조회val categoryIds = businessProfiles.map { it.categoryId }val categories = categoryRepository.findAllBy(categoryIds).associateBy { it.id }// 하나의 Dto로 조합val dtos = businessProfiles.map { Dto( businessProfile = it, category = categories[it.categoryId] )}실제 프로덕션 코드 또한 위의 예시 코드와 비슷한 흐름으로 작성됐어요. 차이점은 조합해야 할 객체 관계가 더욱 방대하고 상황별로 필요한 객체들이 조금씩 다르다는 점이었어요. 예를 들어 위의 코드처럼 카테고리 정보만 필요한 경우도 있었지만, 어떤 경우에는 사업자 검수 여부와 커머스 가입 여부가 동시에 필요했어요.그러다 보니 조인 부분만 조금씩 바뀐 비슷한 Aggregator 객체가 엄청나게 늘어났는데요. 이렇게 중복 코드가 많이 생기면 Circuit breaker, Cache, 동시성 같은 공통의 관심사를 추가할 때 특히 힘들었어요. 변경 누락이 발생해서 의도치 않은 시스템 결함이 생길 위험성도 높아졌고요.그래서 하나의 공통 Aggregator를 만들어 재활용하는 방향성을 가지게 되어요. 물론 이 공통 Aggregator는 매번 모든 관계를 조회하면 안 돼요. 매번 모든 관계를 조인하면 레이턴시도 느려지고 시스템 부하도 매우 커지기 때문이에요. 그래서 include라는 인자를 통해 선택적으로 조인을 할 수 있게 만들었어요. 이해하기 쉽게 코드를 예시로 들면 다음과 같아요.// 카테고리 정보만 필요한 경우aggregator.aggregate( businessProfileIds, includeCategory=true,)// 사업자 검수 여부와 커머스 가입 여부가 동시에 필요한 경우aggregator.aggregate( businessProfileIds, includeBusinessInspection=true, includeCommerceStore=true,)굉장히 뚱뚱한 객체를 가지게 된다는 단점은 있지만 팀은 몇 년 동안 해당 Aggregator를 잘 사용해 왔어요. 그러나 연관 관계가 계속 추가되고 UseCase가 복잡해지면서 여러 어려움을 겪게 되었어요.문제 1. include 유지보수의 어려움조인할 필드가 늘어나고 복잡해짐에 따라 include 옵션이 굉장히 많아졌어요. 아래와 같은 인터페이스를 예시로 들어 볼게요.fun aggregate( businessProfileIds: List<Long>, includeImage: Boolean = false, includeCategorySuggestion: Boolean = false, includeRole: Boolean = false, includePoiId: Boolean = false, includeUserAdminBanner: Boolean = false, includeBusinessRegistration: Boolean = false, includeBusinessAccountBranch: Boolean = false, // ...): BigDTO해당 인터페이스는 시간이 지나면서 Include의 표현력이 떨어지기 쉽다는 위험성을 가지고 있어요. 예를 들어 includeImage는 어떤 객체를 조인하는 것일까요? 처음에는 비즈 프로필에 들어갈 수 있는 이미지는 프로필 이미지밖에 없었기 때문에 직관적으로 프로필 이미지라는 것을 알 수 있었어요. 그러나 시간이 흘러 이미지는 배경 사진, 가격표 사진 등 여러 의미를 가질 수 있게 되었어요.한 번 include를 모호하게 정의하는 실수를 하면 시간이 지나서 되돌리기 매우 힘들었어요. 모호한 include를 다시 세밀하게 만들려면, 모든 호출 코드에서 실제로 배경 사진과 가격표 사진이 필요한지 일일이 파악해야 했기 때문이에요.Include가 모든 조인을 세밀하게 표현하지 못하면 필요 없는 호출이 발생할 가능성이 매우 높아져요. 이로 인해 레이턴시에도 영향이 갔고, 사이드 이펙트를 예측하기 힘들어지는 큰 문제도 생겨요. 섣불리 include 옵션을 건드렸다가 의도치 않은 버그가 생기는 일이 잦아졌어요.저를 포함해 많은 팀원이 해당 객체에 새로운 관계를 추가하거나 include 옵션을 변경하는 데 두려움을 가지게 됐어요. Aggregator를 두려워하는 사람들이 많아지면서 점점 더 위험한 레거시로 성장했어요.문제 2. 예외 처리필드를 조인하다 보면 여러 에러가 발생할 수 있어요. 동일한 조인에 발생한 에러라도 클라이언트마다 필요한 예외 처리가 달라야 했어요. 하지만 대다수의 클라이언트가 중요하지 않게 생각하는 필드는 종종 폴백으로 생략되는 일이 빈번했어요.예를 들어 비즈 프로필에 작성된 후기의 개수를 조회하는 데 실패하면 0으로 채워주고 있었어요. 대다수의 케이스에서는 후기 수 조회에 실패해도 크리티컬 하지 않았기 때문이에요. 그러나 후기 수를 정말 중요하게 취급하는 도메인이 생긴다면 이는 굉장히 위험한 전략이에요. 인터페이스에서 명확하게 에러에 대한 사실이 드러나지 않는 것에 대해 잠재적인 위험성이 크다고 판단했어요.문제 3. 인자 명시성모든 연관 관계가 단순히 비즈 프로필의 아이디로 조인할 수 있는 것은 아니었어요. 예를 들어 현재 유저가 비즈 프로필의 후기를 작성했는지 판단하려면 유저의 아이디가 추가적으로 필요해요. 그러한 이유로 Aggregator는 비즈 프로필 아이디 외에도 유저 아이디를 포함해 여러 인자를 필요로 했어요.여기서 문제점은 굉장히 방대한 Aggregator의 관계 때문에 UserId가 어떤 필드들에 영향을 주는지 파악하는 비용이 커졌다는 것이에요. 여러 함수의 호출을 따라가고 나서야 유저 아이디는 후기 작성 여부, 관리자 여부 등에 영향을 준다는 것을 파악할 수 있었어요. 인자에 대한 사이드 이펙트 추적이 어려워지면서 유지 보수 비용이 증가했어요.문제 4. 동시성과 내결함성Aggregator에서 취급하는 조인이 늘어나면서 동시성에 대한 필요도가 높아졌어요. 순차 호출로는 레이턴시를 보장할 수 없게 되었기 때문이에요. 또한 TPS가 증가하고 서버 간의 의존성이 증가하면서 내결함성도 중요해져요. 그렇지 않아도 복잡한 Aggregator에 쓰레딩, 서킷브레이크, 캐싱등의 로직이 추가되면서 Aggregator를 관리하기가 더 어려워졌어요.QueryFacade 계층 도입기존 Aggregator의 대안을 리서치하다가 넷플릭스의 Optimizing the Netflix API라는 글에서 영감을 받았어요.The API Service Layer abstracts away all backend services and dependencies behind facades. As a result, endpoint code accesses “functionality” rather than a “system”.Netflix의 경우 API 서비스가 QueryFacade 객체를 의존하여 세부적인 사항을 뒤로 숨긴다고 해요.Netflix 블로그에 소개된 QueryFacade는 2013년에 제안된 내용이에요. 모든 내용을 그대로 가져오는 대신 팀 상황에 맞게 필요한 QueryFacade를 새로 정의했는데요. 앞서 설명한 문제를 완화하려면 다음과 같은 조건들을 충족해야 한다고 생각했어요.필요한 맞춤 쿼리를 쉽게 만들어 낼 수 있다.동시성을 지원한다.부분 에러 처리 기능을 지원한다.각 조인에서 필요한 의존성을 쉽게 파악할 수 있다. (ex: 유저 아이디는 유저의 후기 작성 여부를 조회할 때 사용된다.)GraphQL 도입 배경실질적인 구현 상세를 고민하다 보니 GraphQL이 적합한 도구가 될 수 있다고 생각했어요. 먼저 GraphQL이 QueryFacade의 세부 조건을 만족하는지 확인해 봤어요.1. 필요한 맞춤 쿼리를 쉽게 만들어 낼 수 있다.GraphQL 명세는 Query Language라는 DSL을 포함하고 있어요. 예를 들어 다음의 쿼리는 비즈프로필을 지도에 노출하기 위한 데이터를 조인하는 일부 쿼리예요.BusinessProfileMapPreviewQuery(( $userId: Long!, $businessProfileId: Long!) { businessProfile(businessProfileId: $businessProfileId) { // 프로필 이름 name // 프로필 이미지 url profileImage { url } // 프로필의 지역 이름 region { name } // 로컬프로필 상세 화면 진입을 위한 URI, referrer는 지도 targetUri(referrer: "map") }}다만 GraphQL에서 DSL을 지원하더라도 실제 코드에서 활용할 수 있어야 해요. GraphQL 생태계는 각종 프로그래밍 언어에 대해 코드 생성 라이브러리를 지원해요. 예를 들어 Apollo Kotlin라는 라이브러리를 사용하면 위의 쿼리에 대하여 다음과 같은 결과를 얻을 수 있어요.// BusinessProfileMapPreviewQuery은 생성된 코드val query = BusinessProfileMapPreviewQuery( userId = userId, businessProfileId = businessProfileId,)// LocalProfileMapPreviewQuery를 QueryFacade에 넘겨서 결과를 받는다.val result = queryFacade.execute(query)// 프로필 이름result.data.businessProfile.name// 프로필 이미지 URL result.data.businessProfile.profileImage?.urlDSL을 활용하면 필요한 쿼리를 쉽게 만들 수 있어요. 코드 생성 기능을 이용해 코드상에서 GraphQL의 상세 구현을 숨길 수도 있었고요. 따라서 GraphQL이 해당 조건을 만족한다고 판단했어요.2. 동시성을 지원한다.연관 관계를 조인하는 작업은 기본적으로 트리 형태를 가져요. 예를 들어 다음과 같은 그림으로 표현할 수 있어요.하위 노드는 상위 노드의 정보가 있어야만 정보를 조회할 수 있어요. 따라서 트리는 형제 노드 단위로 동시성을 가질 수 있어요. 그러한 관점에서 다음과 같은 요소들이 속도에 영향을 줄 수 있어요.트리의 깊이: 트리의 깊이만큼 대기해야 하는 의존성의 수가 늘어나요.상위 노드의 응답 속도: 위의 예시에서는 BusinessProfile 노드의 리졸빙 속도가 느리면 전체가 대기해야 해요. 대체적으로 상위 노드일수록 응답 속도가 빠른 것이 중요해요.형제 노드의 응답 속도: profileImage 필드가 아무리 빨리 끝나더라도 region 필드가 끝나기 전까지 기다려야 해요.GraphQL 엔진의 경우 여러 동시성 전략을 지원하며 팀에 알맞은 새로운 처리 전략을 구현할 수도 있었어요. 생태계에서 현재 팀에서 필요한 수준의 동시성을 기본적으로 지원하고 있었기 때문에 해당 조건을 만족한다고 봤어요. 또한 Aggregator의 경우 위와 같은 트리 관계를 인지하는 것이 어려는데요. GraphQL의 경우 DSL만 보고도 트리 관계를 쉽게 인지할 수 있다는 부가적인 장점도 있었어요.3. 부분 에러 처리 기능을 지원한다.GraphQL의 경우 필드별로 어떤 에러가 발생했는지 알 수 있어요. 예를 들어 특정 필드에서 에러가 발생하면 다음과 같은 결과를 얻을 수 있어요.{ "errors": [ { "message": "db timeout", "path": [ "businessProfile", "name" ] } ], "data": { "businessProfile": { "name": null, "profileImage": { "url": "https://xxx" } } }}이렇듯 필드별로 어떤 에러가 발생했는지 알 수 있기 때문에, 호출자의 상황에 따라 자유롭게 에러 핸들링을 할 수 있어요.for error in result.errors { if error.path contain(listOf("businessProfile", "name")) { // handle error }}그러나 QueryFacade로 사용하기에 몇 가지 아쉬운 부분이 존재해요.에러가 객체가 아닌 문자열이에요. 문자열로 에러 타입을 구분하는 것은 객체 대비 안정성이 떨어져요. 또한 StackTrace 같은 추가적인 정보를 획득하기 어려워요.error.path가 문자열 기반이에요. 에러가 발생할 필드를 구분할 때 타입 안정성이 떨어져요. 필드 이름을 잘 못 입력하면 에러 핸들링이 누락 될 수 있어요.필드 하나하나 에러 처리를 하는 것은 부담스러워요.이러한 부분을 해결하는 방안은 바로 다음 파트에서 뒤이어 설명해 볼게요. 우선 해당 조건의 경우 GraphQL의 기본적인 기능으로는 완전히 충족되지 않는다고 판단했어요.4. 각 조인에서 필요한 의존성을 쉽게 파악할 수 있다.해당 조건은 GraphQL의 DSL의 강점으로 봤어요.query BusinessProfileQuery( $userId: Long!, $businessProfileId: Long! $referrer: String!) { businessProfile(id: $businessProfileId) { // targetUri 필드와 referrer이 관련 있다는 것을 바로 알 수 있어요. targetUri(referrer: referrer) // 유저 아이디가 사장님에게 작성한 후기 존재 여부를 조회할 때 사용한다는 것을 쉽게 알 수 있어요. hasReview(userId: $userId) } }DSL을 통해 BusinessProfileQuery의 인자가 어떤 필드와 의존성을 가지는지 명확하게 파악할 수 있었어요. 따라서 해당 조건을 만족한다고 판단했어요.GraphQL은 API 서빙 레이어 아닌가?GraphQL은 API나 프론트엔드를 위한 기술이라는 인상이 강해요. 사실 저희 팀 또한 처음에는 QueryFacade를 구현하는 데 있어 GraphQL을 사용하는 것을 고려하지 않았어요. 원래 팀에서는 특정 라이브러리를 사용하지 않고 QueryFacade를 자체 구현하는 방식을 택했어요. 그러다가 구현된 코드가 GraphQL 엔진과 유사한 부분이 많다는 점을 깨닫고 가능성을 검토하기 시작했어요.검토를 마친 후 GraphQL이 제안하는 Resolvers, DataLoader 같은 좋은 구현 패턴과 잘 정의된 DSL을 기반으로 하는 풍부한 툴링 생태계에 이점이 크다고 생각했어요. 따라서 인메모리상에서 사용해도 괜찮다는 결론을 내렸어요. GraphQL은 HTTP, WebSocket 같은 특정 프로토콜과 의존성이 없어요.구체적인 구현 과정팀에서 선택한 핵심 라이브러리는 graphql-java와 apollo-kotlin이에요. 둘 다 JVM GraphQL에서 널리 사용되고 있는 만큼 문서화가 잘 되어 있고 레퍼런스도 풍부해요. 따라서 해당 라이브러리의 기본적인 사용 방법보다는, GraphQL을 QueryFacade로 구현하기 위해 필요했던 추가적인 작업에 관해 설명드려볼게요.1. 에러 핸들링앞서 “부분 에러 처리 기능을 지원한다.”는 조건을 확인하는 데 있어 아쉬운 부분이 있다고 말씀드렸어요. GraphQL에선 이렇게 아쉬운 부분이 있을 때 Directive를 통해 원하는 기능을 추가적으로 정의 할 수 있어요.예를 들어 저희는 errorGroup이라는 커스텀 Directive를 만들었는데요. 다음과 같이 사용되었어요.directive @errorGroup( name: String!) on FIELDquery BusinessProfileMapPreviewQuery( $userId: Long!, $businessProfileId: Long!) { businessProfile(businessProfileId: $businessProfileId) @errorGroup(name = "panic") { // 프로필 이름 name // 프로필 이미지 url profileImage @errorGroup(name = "fallback") { url } }}위의 쿼리를 해석하는 방법은 다음과 같아요.businessProfileId과 businessProfileId 하위에서 발생하는 에러는 전부 panic 그룹 하위에 포함한다.profileImage의 경우 panic 대신 fallback 그룹에 포함시킨다.실제 Kotlin 코드에서는 다음과 같이 처리할 수 있도록 했어요.val result = queryFaade.execute(query)// panic 그룹에 에러가 존재하면 에러를 발생시킨다.result.throwIfHasError(ErrorGroupName.PANIC)// fallback 그룹에 발생한 에러는 log를 찍고 무시한다.for (exception in result.errors[ErrorGroupName.FALLBACK]) { logger.error(exception)}errorGroup Directive 의 구현 요구 사항은 두 가지예요.부모의 errorGroup을 상속받는다.발생한 에러를 가져올 수 있다.위의 요구 사항을 구현하기 위해 사용한 구현 방법은 다음과 같아요.GraphQLContext: 발생한 에러를 저장해요.LocalContext: 트리의 노드에게 개별 컨텍스트를 전달해요.Decorator: 모든 필드에 Decorator를 적용해 errorGroup 처리좀 더 수월한 이해를 위해 핵심 부분을 예시 코드로 작성해봤어요.class ErrorGroupingDataFetcherDecorator( private val original: DataFetcher<*>) : DataFetcher<Any> { override fun get(environment: DataFetchingEnvironment): Any? { val newLocalContext = // get from environment.localContext or Directive try { val result = original.get(environment) return DataFetcherResult.newResult() .data(result) // 새로운 LocalContext를 하위에 전파한다. .localContext(newLocalContext) .build() catch (e: Exception) { // ErrorContext에 에러 추가 (thread safe) environment.graphQlContext.get<ErrorContext>().add( newLocalContext.errorGroupName, e ) } }}2. Cache 제어TPS가 높기 때문에 캐시에 대한 고민도 필요했어요. 개별 API 마다 데이터 최신성이 매우 중요한 경우가 있고 느슨하게 관리해도 될 때가 있었어요. 중요도에 따라 캐시 무효화 정책을 가져가는 경우도 있고 TTL에 의존하는 경우도 있었는데요. TTL 또한 Directive를 이용해 제어할 수 있어요.query LocalProfileMapPreviewQuery( $userId: Long!, $localProfileId: Long!) { localProfile(localProfile: $localProfileId) @errorGroup(name = "panic") { // 프로필 이름 name // 프로필 이미지 url profileImage @errorGroup(name = "fallback") @cache(scope: LOCAL, minutes: 5) @cache(scope: DISTRIBUTED, minutes: 10) { url } }}cache는 errorGroup과 유사한 방법으로 구현할 수 있기에 구체적인 코드는 생략할게요.도입 과정에서의 문제 해결QueryFacade를 통해 기존 문제점들에 대응할 수 있었지만, 예상치 못한 문제를 새롭게 마주하기도 했어요. 실제로 QueryFacade를 프로덕션에 적용했을 때, p50의 레이턴시가 약 2배 줄었지만 p99가 2배 가량 늘어났는데요. 그 원인을 아래 두 가지로 파악했어요.원인 1. 필드 수에 의한 부하graphql-java의 경우 필드 수가 늘어날수록 부하가 증가하는 이슈가 있었어요. 일부 원인은 다음과 같아요.내부적으로 생성되는 객체 수가 필드 수와 비례해요.직렬화 / 역직렬화할 필드가 늘어나요.특히 중첩 배열 상태일 때, 아래의 쿼리 또한 큰 부하를 발생시킬 수 있어요.query BusinessProfileQuery( $businessProfileIds: [Long!]!) { // 로컬프로필을 Bulk 조회 businessProfiles(ids: businessProfileIds) { // 로컬프로필의 이름 name // 배경 사진 목록 backgroundImages { url } }}예를 들어 로컬프로필이 1,000개고 모든 프로필이 배경 사진을 10개씩 가지고 있다고 가정해 볼게요.name 필드가 천 번 실행됨backgroundImages 필드가 천 번 실행됨backgroundImages.url 필드가 만 번 실행됨 (로컬프로필 수 * 배경 사진 수)작은 쿼리라도 순식간에 필드 수가 굉장히 늘어날 수 있기 때문에 p99의 케이스에서는 문제가 발생했어요.원인 2. Query 파싱 부하GraphQL의 Query를 파싱하는 작업 자체에도 1ms~4ms 정도의 부하가 발생함을 확인했어요. 부하가 발생하는 이유는 다음의 작업들이 수반되기 때문이에요.쿼리를 읽어 내부 객체로 변환.쿼리가 문법적으로 올바른지 검사.해결 방법 1. EntityScalarGraphQL Java 엔진에서 필드 수가 성능상의 문제가 됐기에 필드 수 자체를 줄일 방법을 고민했어요. 팀에서 선택한 방법은 Scalar를 사용하는 것이었어요. GraphQL에서 Scalar는 Int, Float, String, Boolean 같은 primitive 타입을 의미하는데요. 원한다면 자제적인 Scalar를 정의할 수도 있어요. GraphQL이 인메모리에서 동작한다는 특성을 살려서 객체 자체를 Scalar로 만드는 방법을 채택했어요.예를 들어 다음과 같이 쿼리를 구성했어요.scalar BusinessProfileScalarscalar ImageScalarquery BusinessProfileQuery( $businessProfileIds: [Long!]!) { businessProfiles(ids: localProfileIds) { // 비즈프로필 객체 자체 (BusinessProfileScalar) businessProfileScalar // 사진 객체 자체 ([ImageScalar]) backgroundImageScalars }}위의 쿼리를 실행한 결과로 얻는 반환 값은 BusinessProfile과 Image 객체예요.// 쿼리 실행val result = queryFacade.execute(query)// 기존에 사용하던 BusinessProfile 모델을 반환val businessProfile = result.businessProfiles[0].businessProfileScalar// 기존에 정의한 메소드 그대로 활용 가능businessProfile.isOwner(user) == true이렇게 쿼리를 구성하면 최악의 경우에도 필드 실행이 2천 번으로 줄어요. 따라서 해당 방식을 적용했을 때 p99를 비약적으로 개선할 수 있었어요. 또한 기존에 정의한 모델을 그대로 사용하기 때문에 코드 관리 측면에서도 훨씬 좋았어요.더 구체적인 사항이 궁금하신 분들을 위해 실제로 EntityScalar를 어떻게 구현했는지 설명드릴게요. 설명을 위해 먼저 두 가지 개념을 이해해야 해요.GraphQL의 Coercing: Custom Scalar를 직렬화/역직렬화하는 책임을 담당하는 객체예요Apollo-GraphQL의 Adapter: Custom Scalar를 CodeGen된 클래스에 값을 넣을 때 사용되는객체예요.Coercing에서 객체를 직렬화하고 Adapter에서 역직렬화할 때 다시 객체로 만드는 것이에요. 직렬화와 역직렬화를 위해 EntityScalarRegistry라는 객체를 만들었어요. 해당 객체의 책임 범위는 다음과 같아요.하나의 쿼리에서 유니크한 객체 ID를 발급해요.쿼리 시작 시점에 생성되고 쿼리가 끝나면 제거돼요.실제로 다음과 같이 간단한 객체예요.class EntityScalarRegistry { private val scalars = ConcurrentHashMap<Int, Any>() private val scalarId = AtomicInteger(0) fun resolve(id: Int): Any? { return scalars[id] } fun register(scalar: Any): Int { val id = scalarId.incrementAndGet() scalars[id] = scalar return id }}해당 EntityScalarRegistry를 기반으로 생성된 ID를 GraphQL의 결과를 직렬화할 때 사용해요. GraphQL의 결과를 다시 코드로 표현하는 시점에는 EntityScalarRegistry를 통해 ID를 객체로 변환해요. 그림으로 그 관계를 표현한다면 다음과 같아요.해결 방법 2. Trusted DocumentTrusted Document는 GraphQL 생태계에서 가장 널리 알려진 베스트 프렉티스예요. 퍼블릭 네트워크에서 불특정 다수가 취약한 쿼리를 날리지 않게 막아주는 개념이에요. QueryFacade의 경우 GraphQL이 네트워크로 노출되지 않기 때문에 필요 없는 개념이라고 생각했어요. 하지만 graphql-java의 경우 PreparsedDocument라는 개념과 연관 있기 때문에 상관이 있었어요. PreparsedDocument는 쿼리를 객체화하고 검증하는 작업이 이미 완료된 객체예요. 그렇기 때문에 해당 객체를 이용하면 쿼리 파싱 시간을 제거할 수 있었고 성능 문제를 해결할 수 있었어요.네트워크 레이어에서 Trusted Document를 사용할 때는 워크 플로우를 복잡하게 만들어내는 단점이 있어요. 그러나 인메모리에서 사용하는 경우에는 쿼리 등록, 버전, 호환성 등에 대한 고민이 필요 없기 때문에 비교적 간단하게 구현할 수 있었어요.// graphql-java에서 제공하는 객체들val cache = InMemoryPersistedQueryCache(emptyMap())val preparsedDocumentProvider = ApolloPersistedQuerySupport(cache)return GraphQL .newGraphQL(graphQLSchema) .preparsedDocumentProvider(preparsedDocumentProvider) .build()QueryFacade 도입 후 성과Aggregator의 가장 큰 페인 포인트는 무분별하게 큰 모델이 사용된다는 것이었어요. 편리하기도 했고 매번 작은 DTO를 만들면 중복 코드가 너무 많이 생성되었기 때문이에요. 시간이 지나면서 Aggregator 모델이 수용할 수 있는 복잡도의 한계를 넘어섰고 앞서 언급한 여러 문제가 대두됐어요.QueryFacade를 도입한 결과 DSL을 기반으로 각 케이스에서 정확히 필요한 객체만 쿼리할 수 있게 되었어요. 이제는 작은 DTO를 쉽게 만들 수 있게 되었고 중복 코드 또한 생기지 않게 되었죠. 그로 인해 각 케이스에서 필요한 의존성을 이전보다 빠르게 파악할 수 있게 됐고요. 심지어 필요한 의존성이 변경되더라도 DSL을 통해 쉽게 해소할 수 있었어요. 이러한 점들 덕분에 개발 생산성 측면에서 유의미한 결과를 낼 수 있었어요.또한 QueryFacade를 도입하면서 API 성능 또한 개선할 수 있었어요. 가장 큰 이유는 이전과 다르게 정말로 필요한 의존성만 사용되기 때문이에요. 불필요한 의존성을 제거한 일은 I/O 부하에도 큰 영향을 주었어요. 그로 인해 API 응답 속도는 향상되었고 서버 인스턴스 수와 네트워크 비용 감소에 유의미한 영향을 주게 되었어요. 앞서 언급한 동시성 처리와 캐싱 처리도 한몫을 해주었고요. 대표적인 성능 개선은 다음과 같은 것들이 있었어요.[전체 응답 속도]p95: 45ms → 18ms[Top Call Grpc 응답 속도]p95: 45ms → 14msp50: 18ms → 5ms[홈 상세 HTTP API응답 속도]p95: 100ms → 50msp50: 73ms → 34ms마무리하며로컬비즈니스실은 사장님 도구를 만드는 서비스적인 성격과 당근의 여러 팀의 요구사항을 받는 플랫폼 팀의 성격을 둘 다 가지고 있어요. 여러 팀이 의존하는 만큼 하위 호환성이 중요한 경우도 많고요. QueryFacade 또한 하위 호환성을 지키면서 내부 복잡도를 줄이기 위해 나온 솔루션이기도 해요.아직 만들어야 할 서비스가 많지만 이미 만들어온 서비스 또한 많아요. 이제는 파편화된 서비스를 통합하고 쌓여 있는 레거시를 정리하는 일의 중요성도 높아졌어요. 기존의 복잡한 시스템을 명확하게 정리하는 일을 좋아하신다면 로컬 비즈니스실에서 정말 즐겁게 같이 일할 수 있을 것 같아요. 관심이 있으신 분들은 아래 채용 공고를 통해 언제든지 지원해 주세요!👉 로컬 비즈니스실 채용 공고 보러가기GraphQL을 이용한 QueryFacade 개발기 was originally published in 당근 테크 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.
![[DAN 24] 서치피드: SERP를 넘어 SURF로! 검색의 새로운 물결](https://d2.naver.com/content/images/2024/12/01.png)
네이버 통합 검색은 2023년에 서치피드를 출시하여 통합 검색 모바일 하단에서 피드 형태로 개인화된 콘텐츠를 제공했습니다. 이 서비스의 내부 프로젝트명은 SURF(Search User Recommendation Feed)로, 검색 결과 페이지(Search Engine Results Page, SERP)의 한계를 뛰어넘어 사용자가 마치 바다에서 서핑을 즐기듯 끊임없이 새로운 콘텐츠를 발견하고 탐색할 수 있는 경험 제공을 목표로 하고 있습니다. 이 글에서는 SURF의 도입 배경과 SURF에 적용된 핵심 기술에 대해 설명하겠습니다. 서핑을 즐기듯 끊임없이 새로운 콘텐츠를 만나는 검색 결과 SURF 기존의 검색 경험(SERP)은 사용자가 검색어를 입력하면 출처 또는 의도 단위로 나열되어, 필요할 때 정확한 정보를 빠르게 찾을 수 있다는 장점이 있었습니다. 하지만 연속적이거나 다양한 정보 탐색 니즈를 충족하며, 발견하고 탐색하는 경험으로 확장하기 위해 새로운 방식으로 접근했습니다. SURF는 사용자의 검색 서핑 경험에 다음 3가지 핵심 기능을 제공합니다. A. 신선하고 다양한 파도 가져오기 SURF는 사용자에게 다층적인 콘텐츠 경험을 제공합니다. 먼저 입력된 검색어와 직접 관련된 콘텐츠를 제공하면서, 동시에 사용자의 취향과 관심사를 반영한 추천 결과를 함께 보여줍니다. 예를 들어, 특정 주제의 인기 카페 글이나 최근에 사용자가 클릭했던 문서와 유사한 글을 자연스럽게 노출합니다. 이를 통해 사용자는 원하는 정보를 찾으면서도 새로운 콘텐츠를 발견하는 즐거움을 경험할 수 있습니다. B. 그라데이션 방식으로 정보 제공하기 SURF의 특징적인 기능 중 하나는 그라데이션 방식의 콘텐츠 제공입니다. 사용자가 검색 결과를 아래로 스크롤할 때, 상단에서는 검색어와 가장 밀접하게 연관된 문서가 노출됩니다. 그러다가 점차 아래로 내려갈수록 주제가 자연스럽게 확장되어, 연관성은 있지만 보다 폭넓은 맥락의 콘텐츠가 제공됩니다. 이러한 그라데이션 구조는 사용자가 자연스럽게 관심 영역을 확장하면서 새로운 정보를 탐색할 수 있도록 돕습니다. C. 실시간 피드백 기반으로 동적 최적화하기 SURF는 사용자의 모든 행동을 실시간으로 분석하고 이를 즉각 검색 결과에 반영합니다. 문서 클릭, 스크롤 패턴, 체류 시간 등 다양한 사용자 행동 데이터를 수집하고, 이를 기반으로 사용자의 현재 관심사와 의도를 정교하게 예측합니다. 예를 들어, 특정 주제의 문서를 연속하여 클릭한 사용자에게는 해당 주제와 관련된 더 많은 콘텐츠를 제공하며, 특정 콘텐츠를 건너뛰는 경향을 보이는 경우에는 다른 주제의 콘텐츠 비중을 높이는 방식으로 작동합니다. 또한, SURF는 단순한 키워드 매칭을 넘어 사용자의 검색 맥락을 깊이 있게 이해하고자 합니다. 같은 검색어라도 사용자의 이전 검색 기록, 클릭 패턴, 관심사 등에 따라 다른 맥락의 결과를 제공할 수 있습니다. 특히 동음이의어나 다양한 맥락이 있는 검색어의 경우, 사용자의 실제 의도에 부합하는 결과를 제공하기 위해 개인화된 맥락 분석을 수행합니다. SURF의 가장 큰 특징은 검색이 단순한 정보 찾기를 넘어 지속적인 탐색 경험으로 확장된다는 점입니다. 사용자가 명시적으로 새로운 검색어를 입력하지 않더라도, SURF는 현재 관심사를 바탕으로 연관된 다양한 콘텐츠를 계속해서 제공합니다. 이는 마치 하나의 관심사가 자연스럽게 다른 관심사로 이어지는 실제 정보 탐색 과정을 모사한 것으로, 사용자의 정보 발견 여정을 더욱 풍부하게 만들어줍니다. SURF의 기술적 구현 SURF는 다양한 출처의 문서와 다양한 방법론을 사용하여 네이버 사용자의 검색 니즈를 충족시키기 위해 노력하고 있습니다. 전통적인 IR(information retrieval)은 물론, 최신성에 집중한 리트리버, 사용자 피드백 통계 기반으로 동작하는 리트리버뿐만 아니라 발견 확장을 위해 검색 질의 카테고리에 해당하는 다양한 출처의 인기 문서 리트리버도 사용합니다. 이러한 다양한 리트리버를 사용자의 니즈에 맞게 개인화하여 다양한 출처의 문서를 순위화할 수 있어야 합니다. SURF는 뉴럴 랭커를 학습하여 순위화의 품질을 높이기 위해 노력하고 있습니다. 이렇게 SURF에 활용하고 있는 다양한 방법 중 LLM(large language model)을 활용한 방법론을 위주로 설명하겠습니다. LLM 시대에 진입하면서, 검색 시스템 또한 새로운 변화가 필요했습니다. LLM의 가장 큰 특징은 고차원의 추론 능력으로 마치 사람처럼 텍스트의 맥락을 이해할 수 있다는 점입니다. 그러나 LLM은 크기가 크고 처리 속도가 느려 실시간 검색에 직접 활용하기는 어려웠습니다. 이를 해결하기 위해 저희는 LLM의 이해 능력을 담은 대량의 데이터셋을 만들고 이를 기반으로 실시간 검색에 활용 가능한 sLM(small language model)을 학습시키는 방식을 채택했습니다. 이는 증류(distillation)라고도 불리는 방식으로, LLM의 이해 능력을 더 작고 빠른 모델에 전수하는 것입니다. 저희는 디코더 기반의 sLM 임베딩 모델을 개발했고, 백본(backbone)으로는 사내 모델인 HCX를, LLM의 이해 능력이 녹아 있는 대량의 학습셋은 내부 RRA(re-ranking agent)를 활용하여 지도 학습 파인튜닝(supervised fine-tuning, SFT)을 진행했습니다. 이렇게 개발된 문맥 이해 임베딩 모델을 기반으로, SURF는 사용자에게 제공하고 싶은 4가지 가치를 구현하기 위해 노력했습니다. 반응형, 연관, 확장성, 그리고 개인화로 대표되는 이 '4대 파도'가 각각 어떻게 구현되었는지 자세히 살펴보겠습니다. 1. 반응형 파도(Reactive Wave): 사용자가 클릭한 문서와 연관된 문서 추천하기 SURF에서 가장 먼저 착수한 것은 반응형 파도였습니다. 반응형 파도는 단순한 키워드 매칭을 넘어, 클릭한 문서의 맥락을 파악해 연관 문서를 추천합니다. 예를 들어, 축구 관련 문서를 클릭했다면 같은 단어를 포함하지 않더라도 맥락에 관련된 다른 축구 소식을 추천할 수 있습니다. 따라서, 반응형 파도는 특정 문서와 연관된 문서를 리트리빙하는 것이 핵심 과제였습니다. 기존의 텀 매칭 기반 기술에서는 단어가 가장 많이 겹치는 문서를 찾는 방식을 사용했지만, 이는 중의성 문제로 인해 전혀 관련 없는 문서가 노출되는 한계가 있었습니다. 반면 임베딩 기반 리트리빙은 시드(seed) 문서의 맥락을 이해하고, 단어가 겹치지 않더라도 연관성 있는 문서를 효과적으로 찾아낼 수 있습니다. 반응형 파도의 실제 구현은 다음과 같습니다. 먼저 문서 풀에 대한 sLM 임베딩을 사전에 생성해 두고, SURF에서 사용자 클릭이 발생하면 실시간으로 해당 문서를 sLM에 통과시켜 임베딩을 얻습니다. 이후 ANN(approximate nearest neighbor) 검색을 수행하여 연관 문서를 가져오는 방식입니다. 2. 연관 파도(Related Wave): 검색어와 관련된 최신 문서 찾아주기 연관 파도는 검색 질의에서도 LLM의 맥락 이해 능력을 활용하고자 했습니다. 기존 텀 매칭 기반의 검색은 중의성이나 동음이의어 처리에 어려움이 있었기 때문에 이 문제를 해결하기 위해 LLM 임베딩을 활용하여 사용자의 실제 의도에 맞는 문서를 보여주고자 했습니다. 즉, '손흥민'을 검색하면 손흥민의 실제 경기 내용을 담은 문서를 제공하는 것이 목표였습니다. 하지만 SURF가 주로 다루는 짧은 숏헤드 질의는 문맥을 파악하기에 정보가 너무 부족했기에, 이를 해결하기 위해 '맥락텍스트'라는 개념을 도입했습니다. 예를 들어 '손흥민'에 대한 맥락텍스트는 "손흥민 맨시티 크리스탈팰리스 선발"과 같이 주요 토큰을 연결하여 생성합니다. 맥락텍스트는 반드시 하나일 필요는 없으며, 여러 개를 생성하여 검색의 다양성을 확보할 수 있습니다. 더 나아가 개인별로 다른 맥락텍스트를 생성함으로써 검색 개인화까지 구현할 수 있습니다. 하지만 ANN 사용 과정에서 새로운 과제가 발견되었습니다. 연관 파도는 '연관성 있는 최신 문서'를 제공하는 것이 핵심인데, ANN은 콘텐츠의 관련도만을 기준으로 검색하기 때문에 ANN으로 1차 검색된 문서 풀 내에서 최신 문서를 찾는 방식으로는 최신 문서를 원하는 만큼 가져올 수 없었습니다. 따라서 반응형 파도와는 다른 구조를 채택했습니다. 새로운 방식은 '손흥민'을 검색했을 때 다음과 같이 동작합니다. 문서 풀에서 '손흥민'으로 색인하여 최신 문서 K개를 최신순으로 가져옵니다. '손흥민' 맥락텍스트에 대한 임베딩과 최신 문서 K개의 임베딩을 구합니다. 맥락텍스트와 각 문서 간 코사인 유사도(cosine similarity)를 계산하여 유사도 기반 필터링(cut-off)과 재순위화(re-ranking)를 수행합니다. 이 방식을 통해 '손흥민'으로 색인된 최신 문서 중에서 실제로 손흥민의 경기와 관련된 문서만을 선별하여, 연관도가 높은 순서로 결과를 제공할 수 있게 되었습니다. 3. 확장형 파도(Expansive Wave): 검색어와 관련된 다른 주제로 확장하기 세 번째로 구현된 확장형 파도는 검색 질의와 유사한 다른 질의의 문서를 추천하는 기능입니다. 예를 들어, '손흥민' 검색 시 이강인, 김민재, 홍명보 등과 관련된 문서를 함께 제공하는 것입니다. 이를 '확장 질의'와 '확장 문서'라고 부릅니다. 확장형 파도에서도 동음이의어 문제는 중요한 과제였습니다. '손흥민' 검색에서 '김민재'가 확장 질의로 선정되었을 때, 축구 선수가 아닌 배우 김민재의 문서가 노출된다면 사용자 경험을 해칠 수 있기 때문입니다. 이를 해결하기 위해 맥락텍스트와 sLM을 다시 한 번 활용했습니다. 확장형 파도는 먼저 고품질 질의 풀을 선정하고, 질의의 메타 정보, 패턴, 로그 등을 활용하여 지식 그래프(Knowledge Graph)를 구축합니다. 이 지식 그래프 내에서 검색 질의와 확장 질의 간 매핑이 이루어지며, 동음이의어의 경우 검색 맥락에 따라 적절한 맥락텍스트가 생성됩니다. 예를 들어 '손흥민' 관련 확장 문서를 찾을 때, 지식 그래프에서 '김민재'가 확장 질의로 매핑되면, 축구 선수 김민재에 관한 맥락텍스트가 생성됩니다. 이를 기반으로 ANN 검색을 수행하면 축구 관련 확장 문서만을 제공할 수 있습니다. 더 나아가 지식 그래프와 사용자의 맥락을 결합하여 개인화된 확장 질의 매핑을 생성할 수도 있습니다. 4. 개인화 파도(Personalized Wave): 사용자 행동을 실시간으로 학습하기 마지막 개인화 파도는 랭커를 통해 구현되었습니다. SURF의 랭커는 사용자를 이해하고 피드 스크롤에 따라 적절한 결과를 제공하도록 학습됩니다. 예를 들어 '캠핑' 검색 시, 장소 탐색 의도가 강한 사용자에게는 캠핑장이나 차박 명소를, 장비 탐색 의도가 강한 사용자에게는 캠핑 장비 정보를 우선 제공합니다. 피드 스크롤 중 사용자의 반응도 즉각 반영됩니다. 등유 난로 관련 문서 클릭 시 다음 피드에서는 관련 문서를 적극 추천하고, 우드 테이블 관련 문서를 건너뛰면 해당 주제는 순위가 하향 조정됩니다. 또한 스크롤이 깊어질수록 다양한 주제와 출처로 자연스럽게 확장되도록 설계되어 있습니다. 현재는 더 발전한 형태의 개인화 추천 패러다임을 연구 중입니다. 사용자의 활동 로그를 분석하여 sLM이 관심사를 멀티 프로필로 생성하고, 이를 기반으로 개인화 추천을 제공하는 방식입니다. 추천된 결과에 대한 사용자 반응이 다시 로그로 쌓이면서 지속적으로 프로필이 발전하는 선순환 구조를 만드는 것이 목표입니다. 이러한 에이전트는 사용자의 멀티 프로필을 sLM 임베딩으로 보유하고, ANN 검색을 통해 문서를 피드에 노출합니다. 노출된 문서를 사용자가 클릭하거나 건너뛰는 등의 반응에 따라 해당 프로필의 임베딩을 업데이트하거나 삭제하면서 지속적으로 사용자의 선호도를 학습해 나갑니다. 이를 통해 더욱 정교한 개인화 검색 결과를 제공할 수 있을 것으로 기대됩니다. SURF의 미래: 개인 맞춤형 검색 에이전트를 향해 SURF는 출시 이후 3개월이라는 짧은 시간 동안에 많은 발전을 이루었고, 빠르게 변화하는 기술 환경에 발맞추어 더 큰 진화를 준비하고 있습니다. 검색 서비스의 미래는 단순히 원하는 정보를 찾아주는 것을 넘어서야 합니다. SURF가 그리는 미래의 검색은 개개인의 맥락을 이해하고 함께 고민하는 '나만의 검색 에이전트'입니다. 이 에이전트는 다음과 같은 역할을 수행하게 될 것입니다. 필요한 문서의 핵심을 요약하여 제공 대화형 인터페이스를 통한 심층 정보 탐색 지원 쇼핑 과정에서 제품 탐색부터 구매 결정까지 통합 지원 상황과 맥락을 고려한 적시의 정보 제공 이처럼 SURF는 검색이라는 행위를 더욱 자연스럽고 풍부한 경험으로 발전시키고자 합니다. 검색이 단순한 정보 찾기를 넘어 사용자의 목적 달성을 위한 종합적인 동반자가 되는 것, 그것이 SURF가 그리는 미래의 모습입니다. 우리는 이러한 미래 검색 경험을 실현하기 위해 끊임없이 연구하고 발전해 나갈 것입니다. SURF를 통해 펼쳐질 새로운 검색의 미래에 많은 관심과 기대 부탁드립니다. 이 글은 TEAM NAVER CONFERENCE 'DAN 24'에서 발표한 내용을 토대로 작성되었으며, 발표 내용과 자료는 DAN 24에서 보실 수 있습니다.

안녕하세요! 당근 피드실 피드인프라팀 카터예요.홈 피드 화면피드실은 당근의 첫 화면을 통해 사용자들과 다양한 서비스를 연결해요. 중고거래, 동네생활 모임, 알바, 부동산 등 당근의 여러 서비스가 만드는 콘텐츠들을 사용자에게 재미있게 전할 수 있도록 홈 피드를 구성하고 있죠.피드 아이템피드 화면에서 볼 수 있는 하나하나의 콘텐츠를 의미하는 피드 아이템은 크게 두 부분으로 이루어져 있어요. 첫 번째로 피드 엔티티는 앱에서 보이는 실제 콘텐츠(중고거래 게시글이나 당근알바 게시글)를 말하고, 뷰타입은 이 콘텐츠를 어떤 모양으로 보여줄지 정하는 방식이에요. 뷰타입은 피드에서 피드 엔티티를 시각적으로 표현하기 위해 정의된 개념이고, UI 디자인과 스키마를 속성으로 가진다는 특징이 있어요.피드인프라팀에서는 Server Driven UI를 통해 새로운 피드 아이템 구성을 앱 업데이트 없이도 홈 피드에 빠르게 적용할 수 있도록 시스템을 구축했어요. 이번 글에서는 이 과정에서 어떤 기술적 고민들을 하고 어떻게 해결했는지 소개해 드리려고 해요.피드 아이템에는 어떤 문제가 있었나요?뷰타입 재사용의 어려움당근 초기에는 중고거래 게시글을 기반으로 다양한 형태의 뷰타입을 만들었어요. 이후 부동산, 중고차, 당근알바와 같은 신규 서비스들이 성장하면서, 중고거래 게시글과 비슷한 형태지만 기능과 노출 방식이 다른 뷰타입을 계속 추가했어요. 예를 들어 중고거래 게시글은 중고거래 피드 엔티티를 활용해서 중고거래 뷰타입을 유저에게 노출하고, 당근알바 게시글은 당근알바 피드엔티티를 활용해서 당근알바 뷰타입을 그리는 형식이었어요.하지만 뷰타입은 클라이언트 배포에 포함되기 때문에 앱 업데이트가 필요해요. 모든 사용자가 앱을 즉시 업데이트하진 않기 때문에, 서버에서는 하위 호환성을 위해 버전 분기 처리를 해야 했죠.실험 유연성과 속도 저하피드의 성장을 위해서는 다양한 형태와 조합으로 UI를 실험해야 했지만, 각 뷰타입별로 노출 가능한 정보들의 제약이 많아 실험 진행에 상당한 어려움이 있었어요. 이러한 제약은 실험의 유연성과 속도를 크게 저해했죠. 예를 들어 부동산 게시글의 거래상태(예약 중, 거래완료)를 표현하려고 했던 실험을 살펴볼까요?부동산 게시글의 거래상태를 표시하기 위해 새로운 컴포넌트가 필요했는데, 아이러니하게도 이미 중고거래 뷰타입에 동일한 기능이 구현되어 있었어요. 하지만 뷰타입 간에 컴포넌트를 재사용할 수 없어서, 부동산 게시글 뷰타입에 같은 기능을 담아 앱 업데이트를 배포해야 했죠. 게다가 뷰타입 배포 후 충분한 사용자 수가 확보될 때까지 기다려야 실험을 시작할 수 있었어요. 이런 복잡한 과정 때문에 간단한 실험 하나를 진행하는 데에만 2주 이상이 걸렸답니다.합의된 명칭 부재로 인한 소통의 어려움불명확한 요소들의 명칭또한 뷰타입의 UI 요소들에 대한 표준화된 명칭이 없어서 팀원들 간 협업에 어려움이 있었어요. 예를 들어 같은 UI 요소를 두고 누군가는 ‘썸네일’이라고 부르고, 다른 사람은 ‘이미지’라고 불렀어요. 또 타이틀 아래 영역을 누군가는 ‘태그그룹’이라 부르고, 다른 사람은 위치상 ‘서브타이틀’이라고 불렀죠. 이런 용어의 불일치로 인해 소통 과정에서 불필요한 시간과 노력이 들었어요.어떻게 해결했을까?이러한 문제점을 해결하기 위해서 우리는 Server Driven UI를 도입하고, 이를 바탕으로 두 가지 핵심 컴포넌트인 ‘피드 아이템 카드’ 와 ‘피드 아이템 제네레이터’를 만들었어요. 시작하기 전에 Server Driven UI를 먼저 설명드릴게요.Server Driven User InterfaceServer Driven User Interface(SDUI)는 서버에서 UI의 구조와 동작을 정의하고 제어하는 방식이에요. 서버에서 UI 명세를 내려주면 클라이언트는 이 명세에 따라 화면을 그리죠. 이렇게 하면 새로운 UI를 실험할 때 클라이언트 앱을 매번 업데이트하지 않아도 된다는 장점이 있어요.예를 들어, 기존에는 홈 피드의 UI를 변경하려면 위에서 설명한 것처럼클라이언트 개발앱 배포사용자들의 앱 업데이트충분한 사용자 수 확보의 과정이 필요했지만, SDUI를 도입하면 서버에서 UI 명세만 변경하면 바로 실험을 시작할 수 있어요.SDUI를 효과적으로 활용하기 위해서는 적절한 수준의 구현이 중요했는데요. 서버에서 모든 구조를 정의할 수 있는 HTML처럼 세밀한 수준의 SDUI도 가능하지만, 우리 팀은 다음 세 가지 원칙에 따라 SDUI를 구현했어요.1. 검증된 레이아웃 기반의 유연성기존 서비스들(중고거래, 부동산, 중고차, 당근알바)의 검증된 레이아웃을 기반으로 유연성을 가져가요. 피드가 중고거래 게시글을 시작으로 성장했기 때문에, 검증된 UX를 해치지 않는 선에서 썸네일 크기, 텍스트 스타일, 섹션 배치 등을 서버에서 제어해요.2. 효율적인 스타일 관리서버에서는 UI 레이아웃과 디자인 시스템의 아이콘 토큰, 컬러 토큰과 같은 넓은 범위의 스타일만 정의하고, 클라이언트는 이에 따라 렌더링해요. margin, border-radius 같은 세부 스타일 값은 실험 영역에서 제외하여 복잡도를 낮췄어요.3. 표준화된 인터페이스클라이언트의 동작을 표준화된 인터페이스로 정의해요. 이는 기존 뷰타입의 문제점을 해결하기 위한 원칙이에요. 예를 들어 특정 도메인에 종속된 뷰타입은 게시글 ID로만 화면 전환이 가능해서 재사용하기 어려웠거든요. 이제는 뷰타입별로 흩어져 있던 클라이언트 동작 처리 방식을 하나로 모으고, 서버에서 UI 관련 동작을 일관되게 제어할 수 있게 됐어요.피드 아이템 카드이러한 SDUI 원칙을 바탕으로 저희 팀은 ‘피드 아이템 카드’를 만들었어요. 피드 아이템 카드는 저희 팀이 정의한 통합 뷰타입으로, 다양한 서비스의 콘텐츠를 일관된 방식으로 보여줘요. 지금부터 피드 아이템 카드가 어떤 구조로 이루어져 있고, 앞서 설명한 뷰타입 재사용과 소통의 문제를 어떻게 해결했는지 소개해드릴게요.Section? Component? Property?Feed Item Card의 구성요소피드 아이템 카드는 Section, Component, Property 세 개의 계층으로 이뤄져요. 각 계층이 어떤 역할을 하는지 하나씩 살펴볼게요.Section, Component, Property 예시가장 상위 계층인 Section은 피드 아이템 카드에서 가장 큰 영역을 차지하는 요소예요. 게시글의 대표 이미지를 보여주는 Thumbnail Section, 게시글의 제목과 내용 등 주요 정보를 보여주는 Info Section이 대표적이죠. 일관된 사용자 경험을 위해 Section의 순서나 위치는 변경할 수 없고, 일부 Section은 필수로 포함해야 해요.중간 계층인 Component는 Section이 의미를 가지도록 돕는 구성요소예요. Component는 독립적으로 동작하는 기본 단위이며, 각각의 Component는 고유한 Property들을 가지고 있어요. 예를 들어 Info Section 안에 있는 Tag Group Component는 텍스트나 배지 같은 Property들로 원하는 정보를 표시해요.Property는 Component의 특성을 결정하는 가장 기본적인 요소예요. Property는 혼자서는 의미를 가질 수 없고, 반드시 Component에 속해 있어야 해요. Tag Group Component를 예로 들면, 텍스트나 배지, 이미지 등의 Property들은 Tag Group이라는 맥락 안에서만 의미를 가져요.Property는 필수 Property와 선택 Property로 나뉘어요. 예를 들어, 게시글의 상태를 나타내는 Status Component에서 ‘예약중’이나 ‘거래완료’ 배지는 선택 Property예요. 반면 가격이나 게시글의 속성을 나타내는 텍스트는 필수 Property로, 항상 표시되어야 하죠.이러한 계층 구조 덕분에 피드 아이템 카드는 높은 유연성과 재사용성을 갖게 되었어요. 예를 들어 동일한 Tag Group Component를 중고거래와 부동산 게시글에서 각각 다른 Property 조합으로 활용할 수 있게 되었죠.또한 모든 구성 요소가 명확한 계층으로 구분되어 있어 디자이너와 개발자 간 소통도 한결 수월해졌어요. 각자의 역할에서 동일한 구조를 바라보며 작업할 수 있게 되었거든요.새로운 기능이나 디자인을 실험할 때도 큰 이점이 있어요. 기존 컴포넌트들을 새롭게 조합하거나 일부 속성만 수정해서, 실험 모수를 확보하지 않고도 빠르게 변화를 줄 수 있게 되었답니다. 처음부터 새로 만들 필요 없이 검증된 컴포넌트들을 활용할 수 있게 된 거예요.피드 액션!기존에는 클라이언트에서 피드의 이벤트를 처리하는 방식이 뷰타입별로 제각각이었어요. 예를 들어, 중고거래 게시글은 클라이언트가 게시글 ID를 파싱해서 상세화면으로 이동했지만, 당근알바 게시글은 정해진 URI로 이동하는 식이었죠.이런 파편화된 액션들을 하나로 모으기 위해 ‘피드 액션’이라는 통합 시스템을 만들었어요. 이를 통해 서버에서 다양한 액션을 일관되게 제어할 수 있어요.구체적인 예시를 들어볼게요. 홈 피드에서 게시글 숨기기 버튼을 누르면:서버에 숨긴 게시글을 저장하는 HTTP 요청을 보내고유저의 피드에서 피드 아이템을 숨기고아이템 숨기기 이벤트를 로깅하는 것을클라이언트에서 한 번에 처리할 수 있어요. 이런 피드 액션 시스템 덕분에 서버에서 피드 아이템 카드의 동작을 더 체계적이고 유연하게 제어하게 됐어요.피드 아이템을 그리기 위한 피드 아이템 제네레이터피드 아이템 카드의 구조를 만들었으니 각 서비스의 정보를 이 카드에 맞게 변환하는 프로젝트가 필요했어요. 이를 위해 ‘피드 아이템 제네레이터’를 만들었죠. 이름 그대로 피드 시스템의 데이터(피드 엔티티)를 클라이언트 화면(피드 아이템)으로 만들어주는 모듈이에요.기존에는 중고거래팀, 알바팀 등 각 서비스팀이 자신만의 방식대로 화면을 구성했어요. 그러다 보니 아래와 같은 문제점들이 발생했죠.비슷한 화면을 여러 팀에서 각각 만들어 리소스가 낭비됐어요.일관된 사용자 경험을 제공하기 어려웠어요.실험이나 신규 기능을 도입할 때 각 팀과 조율이 필요했어요.이제는 피드 인프라팀이 피드 아이템 생성을 전담하면서 여러 장점이 생겼어요.1. 서비스별, 국가별로 다른 UI를 효율적으로 관리할 수 있어요.예를 들어 중고거래에서는 유저와 매물의 거리를 보여주고, 중고차에서는 누적주행거리를 보여줘요.피드 엔티티 서비스에서 각 서비스의 데이터를 독립적으로 관리하기 때문에 이런 차별화가 가능해요.각 서비스의 특성을 살리면서도 일관된 사용자 경험을 제공할 수 있어요.2. 앱 버전에 따른 하위호환을 체계적으로 지원해요.새로운 컴포넌트가 추가되어도 앱 버전에 따라 적절한 컴포넌트를 보여줄 수 있어요.예를 들어 아이콘 형식이 바뀌면 구 버전 사용자에게는 이전 버전 컴포넌트를 보여주고 최신 버전 사용자에게는 새 컴포넌트를 보여줘요.이를 통해 사용자는 앱 업데이트 여부와 관계없이 안정적인 서비스를 이용할 수 있어요.3. 새로운 실험도 안전하고 효율적으로 할 수 있어요.섹션별로 독립적인 구조라 한 부분의 변경이 다른 부분에 영향을 주지 않아요.실험은 최신 버전에서만 진행해 실험 코드가 여러 곳에 흩어지는 것을 방지했어요.중앙화된 관리로 실험 결과를 빠르게 분석하고 적용할 수 있어요.다만 중앙화된 관리 방식에는 단점도 있어요. 각 서비스팀이 원하는 변경사항을 즉시 적용하기 어렵고, 피드 인프라팀의 작업량이 늘어날 수 있죠. 하지만 일관된 사용자 경험과 안정적인 서비스 제공이라는 이점이 더 크다고 판단했어요. 각 서비스팀과 긴밀하게 소통하며 우선순위를 조율하는 방식으로 이런 단점을 최소화하고 있답니다.이렇게 피드 아이템 제네레이터는 단순히 데이터를 변환하는 것을 넘어, 효율적이고 안정적인 서비스 제공의 핵심 역할을 하고 있어요.결과피드 아이템 카드와 피드 아이템 제네레이터 프로젝트를 실제 피드 프로덕트에 적용하면서 큰 변화가 있었어요. 기존에는 새로운 UI를 실험하려면 클라이언트 앱을 업데이트하고 배포하는 과정이 필요했지만, 이제는 서버에서 바로 새로운 UI를 정의하고 실험할 수 있게 되었어요. 실험 준비에 필요한 서버 개발도 1~2주면 충분해 실험까지의 시간이 크게 단축되었어요. 이를 통해 더 많은 아이디어를 빠르게 검증할 수 있는 환경을 만들 수 있었어요.몇 가지 실험 결과를 살펴볼까요?모임 정보노출 실험동네생활 모임은 운동, 독서 등 공통 관심사를 가지고 이웃들과 온오프라인으로 소통하고 만날 수 있는 서비스인데요. 동네생활 모임 활성화 실험에서는 모임의 최근 활동시간을 보여주고 관심 수를 추가하는 변화를 주었어요. 그 결과 클릭수가 대조군 대비 3% 상승했고, 실험 적용까지는 8일(워킹데이 기준)이 걸렸어요. 특히 이 실험에서는 컴포넌트 단위의 코드만으로 변경사항을 적용할 수 있었고, 실험군의 롤아웃도 기존 실험 코드를 실제 코드로 적용하는 것만으로 충분했어요.비즈니스(업체) 소식 UI 실험비즈니스 소식은 지역의 사장님이 작성한 게시글을 홈피드에 노출하는 서비스인데요. 비즈니스 소식 UI 개선 실험에서는 피드 아이템 카드가 지원하는 다양한 컴포넌트를 활용해 비즈니스 소식의 정보를 전달하고자 했어요. 예를 들어 사장님이 작성한 게시글의 본문을 노출하거나, 단골수를 보여주거나, 업체 이름을 보여주는 실험을 진행했어요. 실험 결과 유저들의 단골 맺기 수가 대조군과 5~10% 만큼의 차이를 보였어요. 이런 긍정적인 결과를 바탕으로 유저에게 더 필요한 정보를 전달하기 위한 추가 실험들을 준비하고 있어요.앞으로의 계획당근은 “활발한 교류가 있는 지역사회를 위해 모바일 기술로 가까운 동네 이웃들을 연결”한다는 비전을 향해 나아가고 있어요. 피드 인프라팀은 이를 기술적으로 실현하기 위해 중고거래, 부동산, 중고차, 당근알바 등 당근의 다양한 서비스들을 하나의 홈 피드로 연결하고 있죠. 앞으로도 피드 아이템 카드의 다양한 컴포넌트와 피드 아이템 제네레이터의 실험 기능을 지속적으로 발전시킬 예정이에요.이를 위해 SDUI와 같은 혁신적인 기술로 복잡한 문제를 해결하고 새로운 아키텍처를 설계하며 함께 성장할 백엔드 엔지니어를 찾고 있는데요. 유저들이 좋아하는 콘텐츠를 안정적으로 서빙할 수 있는 더 나은 방식을 고민하고, 직접 작성한 코드로 수많은 이웃들의 일상을 더 풍요롭게 만드는 경험을 해보고 싶지 않으세요? 피드 인프라팀의 문은 활짝 열려 있답니다!👉 피드 인프라팀 엔지니어 지원하기당근 홈 피드, Server Driven UI로 실험 이터레이션 빠르게 돌리기 was originally published in 당근 테크 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.
프론트엔드 개발자라면 한 번쯤 고민해봤을 성능 최적화! 토스 개발자들이 전하는 최적화의 본질과 실무 노하우를 공개합니다. 초기 로딩부터 런타임 최적화까지, 토스의 박서진, 박건영, 조유성 님이 전하는 최적화 사례와 비법을 만나보세요!

 안녕하세요. NHN Cloud NCUI개발팀 이진우입니다...

이 아티클에서는 개발자들에게 개발할 재미와 즐거움을 제공하는 다양한 전처리기들을 소개합니다.

단계별 상태 관리 라이브러리 @use-funnel을 어떻게 만들게 되었는지 소개드릴게요. 문제의식을 공유하고 해결방법을 찾으려는 분들께 도움이 되면 좋겠어요.