grep

웹뷰 다음의 레일을 깔다: 당근이 Lynx를 선택한 이유

당근

2026년 8월 12일

원문에서 보기 ↗

안녕하세요, 당근 프론트엔드코어팀 리더 Tony입니다. 저희 팀은 프론트엔드 엔지니어들이 쓰는 개발 환경과 배포 인프라를 만들어요. 이 글은 레퍼런스가 거의 없던 기술 Lynx를 검토해서 프로덕션 앱에 넣기까지의 기록이에요. 새로운 기술을 도입할지 고민해 본 분이라면 판단의 과정이 참고가 될 거예요.

웹뷰와 성능 문제

당근 앱의 여러 화면은 웹뷰로 만들어져 있습니다. 웹뷰는 화면을 열 때 웹 문서와 데이터를 네트워크에서 가져와 실행하는데, 이 초기 로딩 문제가 오래된 과제였어요. SSR을 활용하면 서버에서 미리 웹 문서를 그리는 식으로 JavaScript 평가와 DOM 조작의 성능 병목을 줄일 수 있어요. 하지만 웹 문서와 초기 데이터를 네트워크로 가져와야 하는 경로 자체는 남습니다.

실제 웹 화면과 비슷한 네이티브 화면을 웹뷰가 로드되기 전에 보여주는 방법도 있어요. 서비스 워커로 클라이언트 앱을 캐싱해 네트워크 요청 이전에 UI를 그리는 방법도 있고요. 다만 이런 최적화는 제품 복잡도를 높입니다. 주어진 제약 안에서 최적화하는 것도 중요하지만, 개발자가 의도한 UI를 첫 프레임부터 그릴 수 있는 실행 환경이 필요했어요.

우리가 정말 풀고 싶었던 문제는 로딩 시간을 몇 밀리초 더 줄이는 데 있지 않았습니다. 사용자가 로딩을 느끼지 못하게 만들고 싶었습니다.

새로운 실행 환경에 필요했던 조건

  1. 사용자가 만나는 첫 프레임에는 분리된 임시 화면이 아닌 실제 제품 UI가 보여야 했습니다.
  2. 네이티브 앱을 유지한 채 화면 단위로 적용할 수 있어야 했습니다.
  3. 프론트엔드 번들은 앱 바이너리와 독립적으로 배포할 수 있어야 했습니다.
  4. 큰 전담 조직 없이 프론트엔드, iOS, Android 엔지니어 한 명씩으로 시작할 수 있어야 했습니다.
  5. 문제가 생기면 기존 웹뷰로 돌아갈 수 있어야 했습니다.

당근 앱 전체를 새로운 크로스플랫폼 프레임워크로 교체하는 일은 목표가 아니었어요. 당근에서 콘텐츠를 만나는 핵심 경험인 홈 피드, 실제 거래가 일어나는 채팅처럼 비즈니스의 중심이 되는 화면은 빠르고 안정적인 네이티브 스택으로 유지해야 했고요. 우리가 찾은 것은 웹뷰가 맡아온 일부 화면을 더 빠르고 깊게 최적화할 수 있는 렌더링 계층이었습니다.

2025년 3월, 한 동료가 슬랙에 Lynx를 소개하는 글을 올렸어요. 제가 찾던 조건과 맞닿은 설계가 하나씩 눈에 들어왔습니다.

Lynx?

Lynx

Lynx는 React와 CSS에 가까운 개발 모델로 iOS와 Android의 UI를 만들 수 있는 크로스플랫폼 기술이에요. 모바일 UI 전체를 Lynx로 구성할 수도 있고, 기존 네이티브 구성 안에 UI 블록으로 넣을 수도 있어요.

Lynx를 검토하며 가장 먼저 살펴본 것은 React나 CSS의 사용법이 아니었습니다. Lynx가 채택한 JavaScript 엔진과 첫 화면의 렌더링 과정부터 눈에 들어왔어요.

1. 작고 빠르게 시작하는 JavaScript 엔진

JavaScript 엔진을 평가할 때 보통 실행 성능부터 비교해요. 복잡한 연산의 처리 속도와 벤치마크 점수도 중요한 기준이지만, 모바일 UI에서는 런타임을 준비하는 비용도 사용자의 로딩 경험에 직접 영향을 줍니다. 사용자는 화면이 열리는 순간을 경험하니까요.

Lynx의 메인 스레드 런타임은 PrimJS(QuickJS)를 사용해요. PrimJS는 작고 애플리케이션에 삽입하기 쉬운 엔진으로 설계됐고, 낮은 런타임 초기화 비용이 주요 특성이에요. 우리가 풀려던 문제에서는 최대 처리량보다 엔진 시작 비용이 더 중요했습니다. 화면 하나를 위해 런타임을 준비하고, 필요한 JavaScript를 실행하고, 첫 UI를 보여주는 과정에 잘 맞는 특성이었어요.

작고 임베드 가능한 런타임은 실행 환경을 직접 구성할 여지도 줘요. 호스트 앱이 노출할 API를 제한할 수 있고, 공통 Polyfill과 자주 사용하는 코드를 런타임 가까이에 둘 수 있어요. 화면 진입 전에 필요한 리소스를 준비하는 구조로 발전시킬 수도 있고요.

통제할 수 있는 실행 환경을 만드는 출발점으로 적합하다고 판단했습니다.

2. 첫 프레임을 위한 Dual-Thread Architecture

ReactLynx의 코드는 메인 스레드와 백그라운드 스레드에서 서로 다른 역할을 수행해요. https://lynxjs.org/react/lifecycle

메인 스레드는 첫 화면을 그리고 이후 UI 변경을 반영해요. 백그라운드 스레드는 완전한 React 런타임을 바탕으로 컴포넌트 생명주기와 부수 효과를 처리하고요. 첫 화면을 백그라운드 작업이 끝난 뒤에 그리는 구조가 아닙니다.

이 구조를 바탕으로 Lynx는 IFR(Instant First-Frame Rendering, 즉각적인 첫 프레임 렌더링)을 제공해요. 번들과 초기 데이터가 준비돼 있다면 메인 스레드에서 제품 UI를 바로 그릴 수 있어요. 흰 화면이나 임시 상태를 거치지 않고 애플리케이션이 의도한 콘텐츠를 첫 화면에 표시합니다.

Instant First-Frame Rendering (IFR) - Lynx

물론 IFR에도 명확한 조건이 있어요. 번들을 비동기로 내려받아야 한다면 다운로드를 기다려야 하고, 첫 화면의 핵심 데이터가 네트워크 요청 뒤에 준비된다면 데이터도 기다려야 해요. Lynx 공식 문서도 번들과 핵심 콘텐츠를 동기적으로 준비할 수 없는 경우 IFR을 달성할 수 없다고 설명합니다.

흥미롭게도 이 제약 덕분에 첫 화면을 제품과 플랫폼 차원에서 설계할 수 있었어요. 번들을 미리 내려받을지, 초기 데이터를 네이티브에서 주입할지, 어떤 UI를 첫 프레임에 포함할지 결정할 수 있었죠. 웹뷰 안에서 로딩을 줄이는 작업과는 최적화의 위치가 달랐습니다.

3. 익숙한 툴체인과 점진적인 통합

Lynx 애플리케이션은 Rspeedy라는 별도의 번들러로 구성할 수 있어요. Rspeedy는 Rspack과 Rsbuild를 기반으로 하며 HMR을 지원해요. 근간인 Rspack은 프론트엔드 엔지니어에게 익숙한 Webpack 생태계와 호환되는 툴체인이라, 기존 프론트엔드 개발에서 사용하던 도구와 개념을 그대로 활용할 수 있었습니다.

Lynx

번들은 앱 바이너리와 분리해 운영할 수 있어요. 당근에서는 Lynx 번들을 CDN으로 제공하고 네이티브 앱이 URL을 통해 가져오도록 구성했어요. 프론트엔드 코드를 수정할 때마다 앱을 새로 배포할 필요가 없는 구조입니다. (예시: https://lynxjs.org/lynx-examples/hello-world/dist/main.lynx.bundle)

기존 앱과의 통합 범위도 선택할 수 있어요. LynxView를 네이티브 View Hierarchy에 넣어 화면 전체를 구성할 수도 있고, 여러 네이티브 View 사이의 일부 영역에만 배치할 수도 있어요. 이렇게 기존 앱을 다시 작성하지 않고 필요한 영역부터 점진적으로 도입하는 방식을 Brownfield라고 부릅니다.

Embedding LynxView into Native View - Lynx

CSS 지원 범위는 호환성 데이터로 확인할 수 있어요. Lynx는 API, 내장 Element, CSS 속성별로 지원 플랫폼과 버전을 공개해요. 웹과 비슷한 문법을 제공하는 것만큼, 웹과 다른 지점을 분명히 설명하는 태도가 중요했습니다.

Lynx

다른 선택지

크로스 플랫폼 영역에는 React Native, Flutter 같은 성숙한 기술들이 이미 자리를 잡고 있어요. 두 기술 모두 큰 생태계와 충분한 프로덕션 사례를 갖고 있고요. 그런데 왜 이 둘이 아니었냐고요? 저는 누가 더 좋은 기술인지 가리고 싶지는 않았습니다. 당근이 풀고 싶은 문제와 더 잘 맞는 쪽을 찾고 싶었어요.

우리에게 필요한 것은 앱 전체를 책임지는 새로운 애플리케이션 프레임워크가 아니라, 화면 단위로 대체할 수 있는 렌더링 계층이었습니다.

vs "React Native"

React Native도 기존 네이티브 앱에 화면이나 View 단위로 점진적으로 도입할 수 있어요. React 생태계와 풍부한 네이티브 모듈은 제품 개발에 큰 강점이고요. 하지만 우리가 만들려던 것은 새로운 앱 개발 환경 전체가 아니라, 기존 네이티브 앱 안에서 제한된 역할을 수행하는 렌더링 계층이었습니다.

이 조건에서 네이티브 모듈을 하나 허용하는 일은 패키지를 설치하는 데서 끝나지 않아요. iOS와 Android의 호환성, 앱의 생명주기와 권한, 보안, 성능까지 장기적으로 소유해야 해요. 특히 프론트엔드 코드를 앱 바이너리와 독립적으로 배포하려면, 원격 번들이 기대하는 네이티브 모듈 버전과 앱에 실제로 들어 있는 버전이 어긋나지 않게 관리하는 일, 점진 배포와 롤백까지 책임져야 하고요. 생태계가 클수록 지원해야 할 통합 범위에 대한 기대도 함께 커집니다. 이런 부담 때문에 일부 라이브러리 사용을 계속 제한한다면, 생태계는 풍부하지만 "필요한 것을 사용할 수 없는 플랫폼"이라는 인식을 줄 위험이 있었어요.

따라서 우리는 생태계의 범위를 좁히는 대신, 호스트 앱이 제공할 기능과 운영 책임을 명시적으로 선택할 수 있는 실행 환경을 원했어요. Lynx의 제한된 표면은 당시 우리에게 부족함이라기보다 통제 가능한 경계에 가까웠습니다.

vs "Flutter"

Flutter도 Add-to-app을 통해 기존 네이티브 앱에 화면이나 View 단위로 점진적으로 도입할 수 있어요. 자체 렌더링 모델은 플랫폼 간 일관된 UI와 예측 가능한 성능을 만드는 데 강점이 있고, Semantics를 통한 접근성 지원도 제공하고요.

다만 당근은 기존 네이티브 앱의 OS 동작과 접근성 기준을 유지하면서, 웹뷰가 담당하던 일부 화면을 React·CSS 개발 모델과 독립적인 번들 배포 흐름으로 대체하려 했어요. Flutter는 OS가 아니라 자체 렌더링 모델로 UI를 그리기 때문에, 기존 네이티브 제품에서 축적한 동작이 그대로 보존되는지는 별도로 검증해야 해요. Flutter를 도입하면 Dart와 Widget 기반의 개발 체계, Flutter용 디자인 시스템, 엔진과 Plugin의 생명주기, 네이티브 View와 혼합된 화면의 접근성을 새롭게 운영하고 검증해야 하고요.

또한 Flutter의 표준 모바일 배포 모델에서는 컴파일된 Dart 애플리케이션 코드와 자산이 앱 바이너리의 배포 주기에 포함돼요. 프론트엔드 번들을 CDN으로 제공하고 앱과 독립적으로 배포하려던 우리의 운영 방식과는 차이가 있었어요. Flutter의 기능이 부족해서라기보다, 당시 당근이 만들려던 제한적이고 통제 가능한 렌더링 계층까지의 거리가 더 멀다고 판단했습니다.

결국 비교한 것은 각 기술이 할 수 있는 기능의 수가 아니라, 당근이 원하는 운영 모델까지 가기 위해 새롭게 소유해야 할 책임의 범위였습니다.

이 판단에는 분명한 한계도 있어요. 두 기술로 같은 수준의 PoC를 진행해 비교한 결과가 아니고, 세 기술의 성능 순위를 정한 평가도 아니에요. 기존 네이티브 앱을 유지하고, 웹뷰가 맡은 일부 화면부터 적용하고, 단 세 명으로 도입을 시작한다는 조건 아래 내린 선택이었습니다.

레퍼런스가 적은 기술을 검증하는 방법

도입 평가에서 가장 큰 불확실성은 시장의 레퍼런스가 적다는 점이었어요. 프로덕션 앱에 새로운 런타임과 렌더링 엔진을 넣으려면 문서의 기능 목록만으로는 부족해요. 런타임이 언제 생성되고 해제되는지, 오류가 어디까지 격리되는지, 네이티브 리소스가 언제 정리되는지, 플랫폼별 구현이 어떻게 다른지 알아야 하니까요.

이런 질문에 답해 주는 블로그나 질의응답은 없었어요. 그럼 어떻게 검증할까요? 답이 없다면 코드를 직접 보기로 했습니다. Lynx의 코드베이스를 내려받아 AI 에이전트를 실행해 특정 API의 구현 위치를 찾고, iOS와 Android의 코드 경로를 비교하고, 런타임의 생명주기를 따라가고, 문서와 코드가 일치하는지 확인했어요.

# 로컬에 Lynx 코드베이스를 Clone
$ git clone https://github.com/lynx-family/lynx

# AI 에이전트 열어서 궁금한 것 물어보기
$ claude
$ codex

코드는 현재 동작에 가장 가까운 진실의 원천입니다. AI 에이전트는 큰 코드베이스에서 내가 찾고 싶은 진실에 접근하는 비용을 낮췄어요. 레퍼런스의 수보다 직접 검증할 수 있는 범위를 더 중요하게 본 이유입니다.

불확실성을 도입 위험으로 만들지 않기

레퍼런스가 적은 기술에는 불확실성이 있어요. 예상하지 못한 문제가 발견될 수 있고요. 하지만 불확실성이 곧 위험을 뜻하지는 않습니다. 위험은 실패했을 때의 피해와, 실패를 발견하고 되돌리는 능력에 따라 달라져요.

같은 기능을 제공하는 웹뷰가 이미 있는 화면부터 Lynx 적용을 시작했어요. 문제가 생기면 기존 웹뷰로 돌아갈 수 있도록 구성했고, 앱 전체를 한 번에 전환하지 않았어요. 실패의 영향을 특정 화면으로 제한하고 복구 경로를 먼저 만들었습니다.

아키텍처로 설명할 수 있는 부분과 PoC로 확인해야 하는 부분도 나눴어요. PoC로 확인할 범위를 좁혀둔 덕분에, 검증이 틀리더라도 영향이 그 범위 안에 갇혀요. 기술의 모든 것을 알고 시작한 것은 아니에요. 그럴 수도 없었고요. 대신 모르는 부분을 확인하는 방법과 실패했을 때 돌아갈 방법을 갖춘 뒤에 시작했습니다.

두 화면으로 진행한 프로덕션 PoC

처음부터 플랫폼 전체를 만들지는 않았어요. 성격이 다른 두 화면으로 PoC를 진행했습니다.

첫 번째는 전체 서비스 보기 페이지였어요. 콘텐츠 변화가 비교적 적은 화면에서 정적인 레이아웃과 초기 렌더링을 검증했어요.

두 번째는 중고거래 카테고리 피드였어요. 실제 데이터를 불러오고 목록을 추가하며 사용자가 긴 시간 스크롤하는 화면이라, 정적인 화면만으로는 알기 어려운 데이터 페칭과 무한 스크롤의 동작을 확인할 수 있었어요. (좌) 전체 서비스 보기 화면 / (우) 중고거래 카테고리 피드

프론트엔드에서는 당시 인턴이었던 Gina가 Lynx 뷰를 구현했고, iOS에서는 Ray가, Android에서는 Luke가 함께했어요. 성공 기준은 다음과 같습니다.

  1. 기존 비즈니스 지표를 악화시키지 않을 것
  2. 첫 콘텐츠가 보이는 시점을 기존 웹뷰보다 개선할 것
  3. iOS와 Android의 프로덕션 환경에서 안정적으로 동작할 것
  4. 문제가 생기면 기존 웹뷰로 돌아갈 수 있을 것

측정 결과

총 190만명을 대상으로 웹뷰 50%, Lynx 뷰 50%를 노출했어요. 이전 화면에서의 터치 시간부터 첫 화면이 그려지기까지의 시간을 측정했어요. (p75 기준)

솔직히 결과만 봤을 때는 변화가 크지 않아 조금 아쉬웠어요. 하지만 비교 대상인 웹뷰는 오랜 기간 여러 최적화를 거친 화면이었고, Lynx는 경험이 적은 상태에서 처음 만든 구현이다보니 최적화가 부족했어요.

최적화가 부족했다는 사실은 점차 경험이 쌓이면서 나중에 알게 되었어요. 번들 최적화, Chunk Splitting, Cache-Control 같은 기본적인 정책도 갖춰지지 않았고, 번들 프리로딩이나 초기 데이터를 첫 프레임에 전달하는 등 수준 높은 최적화도 진행하지 못했어요. 다만, 그런 상태에서도 오래 다듬은 웹뷰보다 더 나은 결과를 냈습니다. 이 결과로 앞으로 개선할 수 있는 폭을 확인했어요.

그리고 PoC 화면은 실제 프로덕션에서 동작했어요. 새로운 JavaScript 런타임과 렌더링 엔진을 앱에 통합한 첫 시도였는데, PoC 기간에 Lynx로 인한 크래시는 발견할 수 없었어요. 이 정도면 다음 단계로 가볼 수 있겠다는 생각이 들었어요.

함께 운영할 수 있는 대안이 생기다

웹뷰의 장점과 비용은 서로 다른 영역에서 체감되고 있었어요. 프론트엔드 관점에서 웹뷰는 빠른 개발과 독립적인 배포를 가능하게 하는 중요한 도구였어요. 모바일 관점에서는 메모리와 앱 성능을 계속 신경 써야 하는 실행 환경이었고요. 어느 한쪽이 틀린 문제는 아니었습니다. 같은 제품을 만들면서 서로 다른 비용을 직접 마주하고 있었을 뿐이에요.

"웹뷰 사용을 줄이자", "더 최적화하자"라는 말만으로는 이 문제를 풀 수 없었어요. 빠른 개발과 배포 흐름을 대신할 현실적인 선택지가 없었으니까요. 웹뷰를 계속 쓰자는 결론만으로는 모바일에서 감당하는 비용이 남았고요. 두 관점을 함께 가져갈 수 있는 대안이 필요했습니다.

Lynx는 그 대안이 될 가능성을 보여줬어요. 프론트엔드에서는 React와 CSS를 활용하고 기존과 비슷한 번들 배포 흐름을 유지할 수 있었어요. 모바일에서는 웹뷰가 아닌 경량 런타임 기반의 선택지를 확보할 수 있었고요.

어느 한 영역만으로 완성할 수 있는 기술은 아니었어요. 프론트엔드에서는 웹뷰와 비슷하거나 그보다 더 나은 개발 경험을 만들고, 운영 중인 제품 화면의 마이그레이션 Path도 만들어야 했어요. 모바일에서는 런타임과 View의 생명주기, 리소스 로딩, 캐시, 네이티브 모듈, 오류 격리를 설계해야 했고요. 배포와 인터페이스, 디자인 시스템까지 연결하려면 같은 문제를 함께 소유해야 했습니다.

Lynx는 프론트엔드와 모바일 중 어느 한 영역의 기술로 남을 수 없었어요. 서로의 구현을 연결해야 실제 프로덕션에서 쓸 수 있었고, 문제가 생겼을 때도 함께 원인을 찾아야 했어요.

저는 이 문화적 변화가 기술 검증만큼 중요했다고 생각합니다. 협업을 위해 더 자주 만나는 것도 필요하지만, 서로 없이는 풀 수 없는 공통의 문제가 생길 때 일하는 방식은 더 크게 달라져요. Lynx는 그런 문제였어요. 하나의 프레임워크를 도입한 것이 아니라, 프론트엔드와 모바일이 함께 만들고 운영할 수 있는 새로운 레일을 만든 셈입니다.

플랫폼으로 만들기

원격 번들을 받아 화면을 그리는 것만 보면 Lynx와 웹뷰가 크게 달라 보이지 않을 수 있어요. 하지만 차이는 이후에 쌓을 수 있는 최적화 지점에서 생깁니다. 첫 화면을 만든 뒤에는 여러 서비스 팀이 편하고 안정적으로 개발할 수 있는 환경을 구축했어요.

1. Warp 위에 번들 배포 흐름을 만들다

Lynx 번들을 위해 새로운 배포 플랫폼부터 만들지는 않았어요. 대신 프론트엔드코어팀이 만든 Warp를 활용했어요. Warp는 정적 웹 호스팅 플랫폼으로, 빌드된 Lynx 번들은 CloudFront CDN으로 제공돼요. 네이티브 앱은 전달받은 URL로 Lynx 번들을 불러와 화면을 렌더링합니다.

Lynx 엔진은 호스트 애플리케이션이 주입한 리소스 로더를 통해 번들을 가져올 수 있어요. 덕분에 기존 앱의 다운로드 및 웹 캐시 정책에 맞춰 로딩 계층을 구성할 수 있었어요. 프론트엔드의 개발과 배포 흐름을 유지하면서 네이티브 UI를 만드는 기반이 됐습니다.

2. 번들 캐시의 최신성과 시작 비용을 함께 다루다

Lynx 번들에는 ETag와 max-age를 조합한 캐싱을 적용했어요. max-age로 정한 유효기간에는 네트워크 재검증 없이 로컬 번들을 사용해요. 재검증이 필요한 시점에 ETag가 일치하면 전체 파일을 다시 받지 않고 최신 여부만 확인하고요. 네이티브 로더의 정책과 결합하면 제한적인 오프라인 환경도 지원할 수 있습니다.

stale-while-revalidate 정책도 검토할 수 있어요. 캐시된 화면을 먼저 보여주고 백그라운드에서 새 번들을 확인하는 구조예요. 최신성을 유지하는 방식과 첫 화면을 빠르게 여는 방식 사이에서, 제품 특성에 맞는 정책을 선택할 수 있습니다.

3. 공통 실행 자산을 재사용하다

공통 Polyfill을 모든 화면 번들에 반복해서 넣을 필요는 없어요. 여러 Lynx 화면이 같은 버전의 불변 JavaScript 자원을 바라보게 만들면 한 번 받은 자산을 다시 사용할 수 있어요. 콘텐츠가 바뀔 때만 URL이 바뀌도록 버전화하고 긴 캐시 정책을 적용하면 캐시 적중률도 높아지고요. 이 구조에서는 각 화면의 제품 코드를 독립적으로 배포하면서 공통 실행 환경을 공유할 수 있습니다.

4. 자체 DSL로 네이티브 인터페이스를 정의하다

Lynx 화면은 네이티브 앱의 기능을 사용해야 해요. 네이티브 앱에 있는 맥락 정보를 읽고, 다른 화면으로 이동하고, 글쓰기에 필요한 이미지를 고르고, 이벤트 로깅과 분석, 권한이 필요한 기능을 호출해야 하죠.

플랫폼마다 브리지 코드를 수동으로 작성하면 인터페이스가 어긋나기 쉬워요. iOS와 Android, 프론트엔드가 같은 기능을 다르게 이해할 수도 있고요. 이 문제를 해결하기 위해 Langium 기반의 자체 DSL(Domain-specific Language)인 Lydl과 해당 선언이 코드와 통합되는 Codegen 파이프라인을 만들었습니다.

하나의 DSL 정의를 진실의 원천으로 사용하면서 프론트엔드와 iOS, Android에 필요한 인터페이스 코드와 동작을 테스트해 볼 Lynx 번들을 자동으로 생성하고요. 타입과 호출 규약을 한 곳에서 관리해 반복 구현과 수동 작업에서 생기는 오류를 줄였습니다.

module MyModule {
  version "1.0.0";

  error UnknownError = UNKNOWN_ERROR({});

  fn greet(req: { name: string }): string throws UnknownError;
}

docs {
  greet({ name: "Tony" }); // Hello, Tony!
}

5. SEED Design을 연결하다

당근의 디자인 시스템인 SEED Design도 Lynx를 지원하기 시작했어요. Lynx 프로젝트에서 SEED의 디자인 토큰과 스타일, 컴포넌트를 사용할 수 있어요. @seed-design/lynx-react 와 @seed-design/lynx-css 같은 패키지가 당근의 제품 언어를 Lynx 화면에 연결해요.

디자인 시스템 지원은 색상과 타이포그래피를 맞추는 데서 끝나지 않아요. 접근성과 인터랙션을 포함한 제품의 기준을 여러 실행 환경에서 공유하는 일이에요. Lynx 화면이 별도의 실험적인 UI로 보이지 않게 만드는 기반이기도 합니다.

Lynx

배포, 캐싱, 공통 자산, 네이티브 인터페이스, 디자인 시스템이 연결되며, Lynx는 하나의 View에서 여러 팀이 사용할 수 있는 플랫폼으로 바뀌기 시작했습니다.

Brownfield로 확장할 화면을 고르다

당근의 목표는 앱을 Lynx로 다시 만드는 것이 아니에요. 세부 화면을 일괄적으로 마이그레이션할 계획도 없고, 전환 목표를 정해 모든 팀에 적용하는 방식도 생각하지 않아요.

적용 후보는 화면의 특성으로 판단합니다.

따라서 어떤 화면은 계속 네이티브로 남고, 웹뷰가 가장 경제적인 화면도 있을 거예요. 모든 화면을 하나의 기술로 통일하는 일보다 선택지를 확보하고 각 선택의 비용을 낮추는 일이 중요합니다.

앞으로 검토할 최적화도 남아 있어요. 번들 프리로딩, 런타임 준비(Prewarm), 네이티브 초기 데이터 주입의 효과와 비용을 확인해야 해요. 더 많은 화면에 적용하기 전에 관측성과 오류 격리, 번들 및 엔진 버전 호환 정책도 계속 다듬어야 하고요.

다음의 선택지

2025년 3월에는 작은 JavaScript 런타임, 첫 프레임을 중심에 둔 Dual-Thread Architecture, 프론트엔드에 익숙한 툴체인, 기존 앱에 필요한 만큼 넣을 수 있는 구조를 보고 Lynx 도입을 추진하기로 마음먹었어요.

하지만 기술을 선택하는 일과, 여러 조직이 안정적이고 편하게 사용할 수 있는 개발 플랫폼을 만드는 일은 달랐습니다. Gina가 첫 화면을 구현했고, Ray와 Luke가 각각 iOS와 Android의 프로덕션 앱에 런타임을 통합했어요. Warp 위에 번들 배포 흐름을 만들고 캐시와 네이티브 인터페이스를 설계했고요. SEED Design도 Lynx를 지원하기 시작했어요.

Lynx가 모든 화면을 대체하는 정답이라고 생각하지는 않아요. 네이티브의 기반을 유지하면서, 웹뷰가 맡던 일부 화면을 다른 방식으로 만들 수 있게 해 주는 선택지에 가까워요.

당근은 이제 네이티브와 웹뷰 사이에서만 선택하지 않습니다. 화면의 특성에 따라 사용할 수 있는 새로운 실행 환경과, 그 환경을 운영할 레일을 갖추기 시작했어요. 그 레일은 하나의 기술만으로 만들어지지 않았습니다. 프론트엔드와 모바일이 같은 문제를 함께 소유하고 풀어가며 만든 결과입니다.

현재 적용 현황

현재 당근에서는 아래 화면들에 Lynx를 사용하고 있어요. 그리고 현재 여러 팀들이 주요 화면에 Lynx 도입을 계획하고 있어요. 업데이트가 있다면 이 글에 지속적으로 추가할 예정이에요. (2026년 8월 12일 업데이트)

(좌) 공유하기 Bottom Sheet / (중앙) 최근 본 글 / (우) 즐겨찾기


웹뷰 다음의 레일을 깔다: 당근이 Lynx를 선택한 이유 was originally published in 당근 기술 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.