당근 테크 밋업 참여 후기

Essay
2024-11-06

배경

10월 7일 당근에서 주최한 테크 밋업에 다녀왔습니다.

프론트엔드 세션 주제는 아래와 같이 다양했어요.

실무에서 데스크탑 제품만 개발하고 있어 웹뷰를 접할 기회가 없었는데, 이번 당근 테크 밋업에 웹뷰 관련 세션이 3개나 준비되어 있어 큰 기대를 가지고 참여했습니다. 웹뷰에 대해 잘 모르더라도 세션을 듣는 데 어려움이 없었고, 간접적으로 웹뷰에 대해 배울 수 있어 좋았습니다. 뿐만 아니라 웹뷰 외에도 다양한 개념을 다루는 계기가 되어 유익했습니다.

모임 서비스 활용

당근 모임 서비스를 통해 네트워킹 활동과 공지 채팅을 확인할 수 있었습니다. 직접 만든 서비스를 테크 밋업에 적극적으로 활용한 점이 신선하고 좋았습니다👏

세션 영상이 올라오는데도 컨퍼런스에 직접 가는 이유 중 하나는 오프라인으로만 참여할 수 있는 행사들이 있기 때문인데 아쉽게도 당근 네트워킹은 선착순 신청이 너무 빨리 마감되어 참여하지 못했습니다💧

프레임워크부터 플랫폼까지: 당근 웹뷰 플랫폼

당근에 특화된 내용이 많아 당장 업무에 적용할 수 있는 내용은 아니었지만, 웹뷰 개발에 대한 이해를 넓히는 데 큰 도움이 되었습니다. 당근에서 웹뷰를 개발하려면 알아야 하는 3가지 개념인 'Bridge, Framework, Platform'은 아래와 같습니다.

Bridge

Bridge는 웹뷰와 네이티브 사이의 통신을 담당하는 모듈입니다. OS 기능(햅틱 반응)이나 앱의 맥락 정보(현재 로그인한 유저)를 가져올 때 사용합니다. metabridge를 참고하면 될 것 같네요🔍

과거에는 새로운 브릿지 함수를 만들기 위해 '프론트엔드 개발자와 네이티브 개발자가 회의를 통해 인터페이스 구체화 → 프론트엔드 개발자가 TypeScript SDK와 테스트용 웹뷰 작성 → 네이티브 개발자가 테스트용 웹뷰 받아서 구현' 과정을 거치며 많은 소통이 필요했다고 합니다. 현재는 스키마 수정 PR을 올리기만 하면 프리뷰 브릿지 SDK와 테스트용 웹뷰가 자동으로 생성되어 PR 리뷰만으로도 소통할 수 있도록 개선되었다고 합니다.

Framework

당근에서 웹뷰 만들 때 사용하는 stackflow는 프레임워크의 역할을 하고 있습니다. UI life cycle과 routing은 모바일 UI에서 중요한 고려 요소라고 합니다.

UI life cycle

dialog가 열린 상태에서 뒤로 가기를 누르는 경우, PC와는 다르게 모바일에서는 dialog가 닫히는 동작을 기대하게 됩니다. 이처럼 웹과 모바일이 같은 UI를 가지더라도, 디바이스에 따라 기대되는 경험히스토리 관리 방식에 차이가 있다는 것을 알 수 있습니다. 특히 모바일에서는 히스토리 관리가 UX에 큰 영향을 미치기 때문에 모바일에 적합한 라우팅 프레임워크가 필요했다고 합니다.

Routing

URL에는 path, path parameter, query parameter, hash 같은 구성 요소가 포함되어 있습니다. 개발자가 웹뷰 개발을 웹 개발이 아니라 모바일 개발로 느끼게끔 만들고 싶다는 생각에서 모바일스러운 라우팅 개념을 고민하게 되었고, nameparameter로 단순화해서 Activity라는 새로운 개념을 만들었다고 합니다.

as-isto-be
URLActivity
/articles/1234/?referrer=home{ "articleId": 1234, "referrer": "home" }

Platform

웹뷰를 배포하기 위해 당근이 시도했던 방법 3가지와 현재를 알 수 있었습니다.

(1) AWS S3 + CloudFront

스토리지에 정적 파일을 업로드해 웹 서버로 활용할 수 있습니다.
배포, 버전 관리, 프리뷰 배포, 롤백 등의 생산성 기능을 별도 CI로 직접 구축해야 하는 점이 번거롭습니다.

(2) Vercel

Vercel에서 제공하는 다양한 생산성 기능은 개발 속도를 향상시킵니다.
하지만 트래픽 당 과금 정책 때문에 매달 비용이 달라져 예산 관리가 어렵습니다.

(3) Cloudflare Pages

Vercel에서 사용하던 생산성 기능을 대부분 활용할 수 있으며, 많은 트래픽이 발생해도 비용이 증가하지 않습니다.
다만 간헐적으로 다른 국가의 edge로 라우팅되는 이슈가 있습니다.

(4) Warp (현재)

결국 AWS S3와 CloudFront를 활용해 배포 플랫폼 Warp를 자체적으로 구축했다고 합니다. 저라면 플랫폼을 새로 만들 생각조차 못하고 세 번째 시도에서 멈췄을 텐데, 만족스러운 배포 플랫폼을 마주할 때까지 도전한 점이 대단하다고 생각합니다. 마음만 먹으면 배포 플랫폼을 직접 구축할 수 있는 실력 자체가 너무 멋있었고, 직접 구축하자는 의견이 수용되는 개발 문화가 부러웠습니다. 저도 언젠가 이런 개발 문화를 갖춘 곳에서 일해보고 싶다는 생각이 든 부분이었습니다.

결론

지금은 당근 frontend core를 담당하고 있지만, open core로의 확장을 목표로 하고 있다고 하셨는데 당근이라면 생각보다 빨리 이 목표를 이룰 수 있을 것 같다는 느낌이 듭니다.

프론트엔드에게 배포 플랫폼이란 - nothing or everything

첫 번째 세션에서 배포 플랫폼 이야기가 잠깐 나왔는데, 두 번째 세션에서 배포 플랫폼에 대해 더 깊게 다뤄주셨습니다. HTTP 프로토콜부터 인프라까지 웹 시스템 전반을 이해하는 시간이었습니다.

세션에서는 HTML 위치 탐색 → HTML 요청 → HTML 스펙 동작 3가지를 보장하는 것이 배포라고 정의했습니다. 브라우저 동작 순서인 navigate → response → parse → render에서 navigate → response → parse는 앞서 언급했던 HTML 위치 탐색 → HTML 요청 → HTML 스펙 동작 순서와 동일합니다. 브라우저 동작 순서와 배포를 연결지어 생각해본 적이 없었는데, 세션 덕분에 새로운 관점을 얻을 수 있었습니다. 이런 부분에서 배포 플랫폼 제작에 필요한 지식은 백엔드/인프라 지식보다도 의외로 프론트엔드 개발자에게 친숙한 지식들이라고 말씀해 주신 것 같습니다.

내 타입스크립트 코드가 이렇게 느릴 리 없어!

당장 실무에 적용 가능한 꿀팁이 가득했던 세션입니다🍯 다시 보기 영상이 올라오면 이 세션부터 보는 것을 추천드립니다. 타입스크립트 추론 속도가 느려졌을 때의 성능 진단 방법, 분석 도구, 개선 팁들을 소개해 주셨습니다.

타입스크립트 성능 진단

(1) --extendedDiagnostics

  • 타입스크립트 컴파일러가 컴파일 중 소요한 시간을 출력해 주는 CLI 옵션입니다.
  • 각 페이즈별로 소요된 시간을 알 수 있습니다.
    Files:                         6
    Lines:                     24906
    Nodes:                    112200
    Identifiers:               41097
    Symbols:                   27972
    Types:                      8298
    Memory used:              77984K
    Assignability cache size:  33123
    Identity cache size:           2
    Subtype cache size:            0
    I/O Read time:             0.01s
    Parse time:                0.44s
    Program time:              0.45s
    Bind time:                 0.21s
    Check time:                1.07s ← type-checking에 소요된 시간
    transformTime time:        0.01s
    commentTime time:          0.00s
    I/O Write time:            0.00s
    printTime time:            0.01s
    Emit time:                 0.01s
    Total time:                1.75s ← 컴파일에 소요된 시간
    
  • 관련 링크

(2) TypeScript CLI 옵션 --generateTrace

  • 타입스크립트 4.1부터 도입된 CLI 옵션입니다.
  • 컴파일에 소요된 시간 중 오래 걸리는 부분을 쉽게 식별할 수 있도록 trace.json 파일을 출력합니다.
  • 컴파일러가 인식한 타입의 목록을 types.json 파일로 출력합니다.
    몇십만 라인이 나오기 때문에 사람이 분석하기는 어렵고, 분석 도구의 힘을 빌려야 합니다.
  • 관련 링크

(3) @typescript/analyze-trace

  • 타입스크립트 팀에서 제공하는 npm 패키지입니다.
  • CLI로 event trace를 분석해 타입 추론이 오래 걸리는 hot spot을 빠르게 식별 가능합니다.
    tsc -p {프로젝트_경로}/tsconfig.json --generateTrace {event_trace_디렉터리}
    npm install --no-save @typescript/analyze-trace
    npx analyze-trace {event_trace_디렉터리}
    
  • 아래와 같은 output을 확인할 수 있습니다.
    Hot Spots
    ├─ Check file /some/sample/project/node_modules/typescript/lib/lib.dom.d.ts (899ms)
    ├─ Check file /some/sample/project/node_modules/@types/lodash/common/common.d.ts (530ms)
    │  └─ Compare types 50638 and 50640 (511ms)
    │     └─ Compare types 50643 and 50642 (511ms)
    │        └─ Compare types 50648 and 50644 (511ms)
    │           └─ Determine variance of type 50492 (511ms)
    │              └─ Compare types 50652 and 50651 (501ms)
    |                 └─ ...
    ├─ Check file /some/sample/project/node_modules/@types/babel__traverse/index.d.ts (511ms)
    └─ Check file /some/sample/project/node_modules/@types/react/index.d.ts (507ms)
    

(4) perfetto

  • 구글에서 제공하는 event trace 분석 도구입니다.
  • https://ui.perfetto.dev 에 들어가 Open trace file 버튼을 누르고 trace.json 파일을 업로드하면 됩니다.
  • 긴 초록색 블록은 타입 체크에 많은 시간이 소요된 것을 의미합니다.

타입스크립트 성능 개선 팁

세션을 통해 https://github.com/microsoft/TypeScript/wiki/Performance 페이지가 있다는 걸 처음 알았습니다. 타입스크립트 성능 개선을 위한 팁들이 정리되어 있어 정독하면 좋을 것 같습니다.

컴파일 되기 쉬운 코드를 작성하는 방법은 아래와 같습니다.

  1. intersection 대신 interface 사용
  2. type annotation 사용
  3. union type 대신 base type 사용
  4. 복잡한 타입에 이름 짓기

이런 팁을 적용해 타입 추론 속도가 느린 오픈소스에도 기여할 수 있습니다.

웹/웹뷰 코드 합치면 개발 2배 빨라지는 거 아니었어요?

웹뷰뿐만 아니라 분기 처리 때문에 고민했던 경험이 있다면 공감하면서 들을 수 있는 세션였습니다. 이 주제도 마찬가지로 다시 보기 영상이 올라오면 꼭 보는 것을 추천드립니다.

웹뷰와 웹을 별도로 관리하다 보면 '개발 효율 저하, 관리 포인트 증가, 웹뷰 우선 개발로 인한 스펙 차이' 같은 문제가 발생합니다. 이런 문제를 해결하기 위해 당근 광고실도 웹과 웹뷰를 하나의 코드로 통합했습니다. 화면 크기나 런타임 환경에 따라 다르게 동작하는 부분을 처리하기 위해서 분기는 어쩔 수 없이 존재합니다. 분기가 많아질수록 코드의 복잡도는 증가하고 가독성은 감소하므로 통합을 잘하기 위해서 당근 광고실은 어떻게 접근했는지 설명해 주셨습니다.

반응형 UI 코드 품질 높이기

가독성 높게 분기하기 위해서 css 속성 단위로 반응형을 관리하고, 반응형 분기를 위한 컴포넌트를 만들었다고 합니다.

(1) css 속성 단위로 반응형 관리하기

같은 컴포넌트라도 배치나 노출 방식이 다른 경우 보통 media query를 사용합니다.

// cash-info.css
.container {
  display: grid;
  flex-direction: column;
  padding: "16px";
  flex-grow: "1";
}
 
@media screen and (min-width: 768px) {
  .container {
    flex-direction: row;
    flex-grow: "0";
  }
}

당근 광고실은 vanilla-extract를 사용 중이라서 sprinkles 생성 시 conditions에 분기를 설정했습니다.

// sprinkles.css.ts
const responsiveProperties = defineProperties({
  conditions: {
    mobile: { "@media": "screen and (max-width: 767px)" },
    desktop: { "@media": "screen and (min-width: 768px)" },
  },
  /* ... */
});
 
export const sprinkles = createSprinkles(responsiveProperties);
// cash-info.css.ts
import { sprinkles } from "./sprinkles.css";
 
export const container = style([
  { padding: "16px" },
  sprinkles({
    display: "flex",
    flexDirection: { mobile: "column", desktop: "row" },
    flexGrow: { mobile: "1", desktop: "0" },
  }),
]);

이렇게 관리하면 media query 방식과 달리, 각 속성이 mobile과 desktop에서 어떤 값을 가지는지 한눈에 확인할 수 있어 영향 범위 파악이 쉬워집니다.

(2) 반응형 분기를 위한 컴포넌트 개발하기

모바일 뷰와 데스크탑 뷰에서 다른 컴포넌트를 보여줘야 하는 경우가 있는데, 이런 경우를 대응하기 위해 <Responsive /> 컴포넌트를 만들었다고 합니다.

<Responsive desktop={<데스크탑 />} mobile={<모바일 />} />

로직과 UI 분리하기

로직은 <Responsive /> 컴포넌트를 사용하는 상위 컴포넌트에서 작성합니다. 로직과 UI가 독립된 책임을 가지니 유지보수가 쉬워지고, UI 재사용성이 높아집니다.

여러 가지 런타임 대응하기

런타임별로 다른 동작을 처리해야 하는 경우가 있습니다. 예를 들어 당근 웹뷰는 Karrot Analytics를 사용하지만, 웹은 Amplitude와 Google Analytics를 사용합니다. 당근 광고실은 전략 패턴을 활용해서 런타임별 로깅을 구현했다는 점이 흥미로웠습니다.

테스트 작성

테스트 코드가 직접적인 코드 참조를 많이 가지면 가질수록 모킹 대상이 늘어납니다. 테스트 용이성을 위해 React Context를 의존성 주입의 도구로 활용했습니다.

// as-is (import)
 
import { loggingService } from './logging-service';
import { linkService } from './link-service';
 
describe('유튜브버튼_싱글톤', () => {
  it('클릭하면, openInternal과 logEvent가 호출된다.', () => {
    async () => {
      const user = userEvent.setup();
      render(<유튜브버튼_싱글톤 />);
 
      const button = await screen.findByText('유튜브버튼');
      await user.click(button);
 
      expect(loggingService.logEvent).toHaveBeenCalledTimes(1);
      expect(linkService.openInternal).toHaveBeenCalledTimes(1);
    }
  });
});
// to-be (React Context)
 
describe('유튜브버튼_의존성주입', () => {
  it('클릭하면, openInternal이 호출되고 logEvent가 호출된다.', () => {
    async () => {
      const user = userEvent.setup();
      const logEvent = vi.fn();
      const openInternal = vi.fn();
 
      render(
        <LoggingServiceProvider messages={{ logEvent }}>
          <LinkServiceProvider messages={{ openInternal }}>
            <유튜브버튼_의존성주입 />
          </LinkServiceProvider>
        </LoggingServiceProvider>
      );
 
      const button = await screen.findByText('유튜브버튼');
      await user.click(button);
 
      expect(logEvent).toHaveBeenCalledTimes(1);
      expect(openInternal).toHaveBeenCalledTimes(1);
    }
  });
});

정리하기

조건에 따른 여러 구현에 대응할 때 도움이 될 질문 몇 가지를 함께 공유해 주셨습니다.

  • 노출될 인터페이스는 어떤 모습이어야 할까요?
  • 구체적인 구현은 어느 시점에 결정될 수 있나요?
  • 테스트를 어렵게 하는 직접 참조가 있다면 의존성 주입으로 변경할 수 있나요?

개발을 하다 보면 이런 상황을 자주 만나게 되는데, 접근 방식을 참고할 수 있어 유익했습니다.

아니, 여기도 웹뷰였어요?

원래 네이티브 기반이던 동네생활 서비스를 웹뷰로 전환했다고 합니다. 새로운 기능을 넣을 때 A/B 테스트를 많이 하는 편인데, 네이티브 대비 웹뷰에서 훨씬 더 많은 실험이 가능했기 때문입니다. 웹뷰 전환 작업을 시작할 때 가장 중요했던 요구사항은 '느리지 않아야 한다'였을 정도로 속도 문제에 민감했는데, 기존 CSR WebApp 방식으로는 만족할 만한 성능이 나오지 않을 것 같아 SSR 도입을 결정했다고 합니다.

직접 구축한 SSR WebApp

Fastify + Vite + React DOM Server 기반으로 SSR WebApp을 직접 구축했다고 합니다. 첫 번째 세션의 Warp처럼 생태계에 이미 존재하는 Next.js, Remix를 사용하기로 타협하지 않고 상황에 맞는 해결 방법을 찾기 위해 노력한 점이 인상 깊었습니다.

SSR은 모든 API 응답이 끝나야 화면을 보여줄 수 있어서 API 속도가 느리면 CSR 대비 화면이 늦게 뜰 수 있다는 단점이 있는데, 이 단점을 극복하고자 Streaming SSR을 도입했습니다. Streaming SSR은 화면을 한번에 그리지 않고 나눠서 그리는 방식입니다. GNB처럼 API 호출과 관련이 없는 부분을 가장 먼저 그리고, API 호출처럼 외부 요소에 의해 렌더링이 지연될 수 있는 부분은 나중에 그립니다.

Suspense 밖에 있는 부분을 Shell이라고 부릅니다. Shell이 준비되면 onShellReady 콜백이 실행되고, pipe 함수를 호출함으로써 스트리밍이 시작됩니다. Shell이 먼저 보이고, Suspense fallback을 보여주다가 렌더링 되는 방식입니다.

웹뷰 전환 그 이후

새롭게 대응해야 할 문제들도 생겼지만, 웹뷰 전환 이후 다양한 실험을 통해 사용자에게 더 큰 가치를 전달할 수 있게 되어 만족스러웠다고 합니다.

소감

중간중간 쉬는 시간이 10분밖에 없었음에도 마지막까지 지치지 않고 몰입하게 만든 테크 밋업이었습니다. 첫 번째 세션에서 잠깐 배포 이야기가 나오고, 이어진 세션에서 조금 더 깊게 배포를 다뤘습니다. 다섯 번째 세션에는 GraphQL 이야기가 나왔고, 여섯 번째 세션에는 웹/웹뷰 통합 덕분에 GraphQL Schema를 한 번만 정의하게 되어 코드 양이 절반으로 감소했다는 이야기가 나왔습니다. 당근의 기술적 도전들을 하나의 흐름처럼 이어서 들을 수 있어 좋았습니다. Q&A가 길어져 듣지 못한 세션도 있었지만, 다시 보기 영상이 올라온다고 하니 꼭 챙겨보려고 합니다.

참고 자료

세션 자료는 당근 노션에서 확인할 수 있어요. 다시 보기 영상은 당근테크 유튜브에 업로드될 예정이라고 합니다.