Skip to content

Interested in AI, automation, blockchain, web and apps

Seoul, KR--:-- GMT
Let’s Talk

Work/Web Development/EN

Apartment Price and Transaction Lookup Webview
Apartment Price and Transaction Lookup Webview

내 집 시세와 아파트 실거래가 조회 웹뷰

앱 심사 없이 배포되는 아파트 시세·실거래가 웹뷰를 만들었습니다

앱 안에서 열리는 시세 화면

앱에 주거 탭을 새로 붙이는 일이었습니다. 사용자가 이미 연동해 둔 우리 집의 시세와 아파트 실거래가를 한 화면에 모아 보여주는 것이 목표였고, 조건이 셋 있었어요.

첫째, 네이티브 화면이 아니라 앱 안에서 열리는 웹뷰여야 했습니다. 둘째, 로그인 화면이 없었습니다. 인증은 앱이 이미 들고 있는 토큰으로 끝내야 했고, 웹이 그 토큰을 받아 오는 시점이 첫 화면이 뜨는 시점을 결정했습니다. 셋째, 같은 경로가 연동한 집이 0채일 때, 1채일 때, 여러 채일 때 다르게 그려져야 했습니다. 자가·전세·월세에 따라 화면의 같은 자리에 들어가는 숫자의 의미도 달라졌고요.

데이터도 한 종류가 아니었습니다. KB부동산·빅밸류에서 온 시세 평균가와 개별 실거래가는 단위도 분포도 다른데, 이 둘을 한 그래프에 겹쳐 그려야 했습니다. 거래가 여러 건인 달도 있고 한 건도 없는 달도 있었습니다.

시스템 경계와 외부 의존
시스템 경계와 외부 의존

웹뷰로 가되 토큰은 서버가 심는다

Express가 index.pug를 렌더하는 서버 셸 + 커밋 SHA로 이름 붙인 번들을 CDN에

앱이 헤더로 넘긴 토큰과 API 호스트를 문서에 인라인으로 심어 내려보내면, 웹은 브릿지를 한 번도 부르지 않고 첫 요청 안에서 인증을 마칩니다. 번들은 webview-cdn.banksalad.com에서 받게 해 문서 응답을 가볍게 유지했어요. 웹뷰라 쿠키를 믿을 수 없었고, 토큰은 앱이 요청 헤더로만 줬습니다.

전역 상태 저장소 대신 SWR

홈·대시보드·상세 세 화면이 전부 같은 /v6/me/actual-assets 목록을 필요로 했습니다. Redux를 얹는 대신 각 화면이 useRealEstateList()를 그냥 부르게 두고, 중복 요청은 SWR 캐시가 합치도록 했습니다. 화면 코드에 로딩·에러 분기만 남아요. 모바일 웹뷰라 번들 예산이 빡빡했고, 상태를 오래 들고 있을 이유가 없는 읽기 전용 화면이었습니다.

화면마다 controller.ts를 따로 두고 JSX에서 계산을 걷어냄

매매·전세·월세에 따라 같은 컴포넌트 자리에 다른 값이 들어갑니다. 이 분기를 순수 함수로 빼면 렌더링 없이 표 만들듯 테스트할 수 있어요. listItemController, detailTopController가 그렇게 나왔습니다. 디자인 시스템(@banksalad/bpl-web) 컴포넌트를 그대로 써야 해서, 마크업을 바꿔 문제를 피할 수는 없었습니다.

배포와 인프라 구성
배포와 인프라 구성

문서 한 번, 번들은 CDN에서

흐름은 문서 한 번, 번들은 CDN에서로 요약됩니다. 앱이 주거 탭을 열면 웹뷰가 webview.banksalad.com/housing/*를 요청합니다. 이때 Banksalad-Access-Token, Banksalad-Application-Version, Banksalad-Application-Name이 헤더로 붙어요. nginx가 받아 사이드카를 거쳐 Express로 넘기면, supportedWebView 미들웨어가 그 헤더를 읽어 index.pug에 값으로 꽂아 렌더합니다. 그래서 브라우저에 도착한 HTML에는 이미 sessionStorage.accessToken과 window.apiHost, window.apiGatewayHost, window.namespaceEnv를 채우는 스크립트가 들어 있습니다. 이 스크립트는 번들 <script> 태그보다 앞에 있어서, React가 시작될 때 인증 정보는 이미 자리에 있습니다. 이 글에서 깊이 파는 것은 웹뷰 인증과 화면별 상태 분리 두 가지입니다. 실거래가 그래프의 축 계산은 뒤에서 짧게만 짚고, A/B 실험 분기와 CI에서 쿠버네티스로 이어지는 배포 파이프라인은 각각 따로 다룰 가치가 있는 이야기라 이 글에서는 접습니다.

핵심 데이터 모델
핵심 데이터 모델

브릿지 왕복 없이 첫 화면에 토큰을 앉히기

웹뷰에서 인증을 붙일 때 가장 먼저 떠오르는 방법은 네이티브 브릿지입니다. 페이지가 뜨면 window.webkit.messageHandlers 나 AnalyticsWebInterface 같은 인터페이스로 토큰을 요청하고, 콜백이 오면 그때 API를 부르는 거죠. 그런데 이러면 첫 API 호출이 브릿지 왕복만큼 뒤로 밀립니다. 브릿지 규약이 없는 구버전 앱에서는 콜백이 영영 안 와서 흰 화면이 남고요. 토큰을 localStorage에 캐시해 두는 우회는 더 나쁩니다. 로그아웃 후에도 남으니까요.

토큰을 웹이 물어보게 하지 않고, 문서에 실어 보냈습니다. 웹뷰가 던지는 헤더를 Express가 읽어 pug 템플릿 변수로 넘기고, 렌더된 HTML의 인라인 스크립트가 sessionStorage와 window에 값을 씁니다. 이 스크립트는 vendors.bundle.js·main.bundle.js 태그보다 위에 있어서 번들이 평가되기 전에 실행됩니다. React에서는 withAppInfo가 이 값이 갖춰졌는지 확인해 준비 전에는 빈 프래그먼트를, 실패하면 ErrorView를 그립니다. 그리고 이 문서만은 절대 캐시되지 않도록 nginx에 expires -1과 Cache-Control: private, no-cache, no-store, max-age=0을 걸고, 대신 커밋 SHA가 붙은 번들만 CDN에 얹었습니다.

토큰을 얻는 데 드는 왕복이 0이 됩니다. 문서를 받아 오는 그 한 번의 요청에 이미 답이 들어 있으니까요. 첫 API 호출이 파싱 직후 시작되고, 브릿지 유무에 따라 화면이 갈리지도 않습니다. 캐시 정책을 문서와 번들로 쪼갠 것도 여기서 필요해집니다. 토큰이 박힌 HTML은 사용자마다 다르니 절대 공유되면 안 되고, 반대로 번들은 내용이 바뀌면 파일명이 바뀌므로 마음껏 오래 캐시해도 안전합니다. 무겁고 잘 안 바뀌는 것만 CDN에, 가볍고 사용자마다 다른 것만 오리진에 남긴 셈입니다.

억 단위 실거래가를 좁은 축에 담기

차트 라이브러리에 축을 맡기면 아홉 자리 라벨이 좁은 모바일 화면에서 겹칩니다.

util/graph.ts의 순수 함수들이 억·천만·만 중 하나를 단위로 골라 축의 양 끝을 그 단위에 스냅하고, 값이 하나뿐이라 양 끝이 같아지면 눈금을 하나 더해 축을 살립니다. 눈금 간격도 값의 폭에 따라 갈랐습니다.

라벨이 항상 짧게 떨어져 겹치지 않고, 거래가 한 건뿐인 단지에서도 축이 무너지지 않습니다. 축 계산의 세부는 따로 다룰 이야기라 여기서는 결과만 적어 둡니다.

유저가 할 수 있는 일

앱에서 주거 탭을 열면 로그인 없이 우리 집 화면이 뜬다
앱에서 주거 탭을 열면 로그인 없이 우리 집 화면이 뜬다

아직 연동한 집이 없어 주소로 우리 집을 등록하러 간다
아직 연동한 집이 없어 주소로 우리 집을 등록하러 간다

홈에서 우리 집의 최근 실거래가와 시세 그래프를 본다
홈에서 우리 집의 최근 실거래가와 시세 그래프를 본다

집이 여러 채면 홈이 최근 실거래가 목록으로 바뀐다
집이 여러 채면 홈이 최근 실거래가 목록으로 바뀐다

단지의 실거래 내역을 날짜순으로 훑어본다
단지의 실거래 내역을 날짜순으로 훑어본다

기간을 골라 실거래 내역을 좁혀 본다
기간을 골라 실거래 내역을 좁혀 본다

대시보드에서 우리 집 한 채의 기본정보를 확인한다
대시보드에서 우리 집 한 채의 기본정보를 확인한다

여러 채의 평가 손익을 한 줄로 합산해 본다
여러 채의 평가 손익을 한 줄로 합산해 본다

대시보드가 비어 있어 우리 집 연동을 시작한다
대시보드가 비어 있어 우리 집 연동을 시작한다

목록에서 집 하나를 골라 상세 화면을 연다
목록에서 집 하나를 골라 상세 화면을 연다

상세에서 편집하기를 눌러 앱의 자산 편집으로 넘어간다
상세에서 편집하기를 눌러 앱의 자산 편집으로 넘어간다

이미 연동한 집이 있는 상태에서 한 채를 더 추가한다
이미 연동한 집이 있는 상태에서 한 채를 더 추가한다

홈 하단 배너가 실험 배정에 따라 다른 상품으로 바뀐다
홈 하단 배너가 실험 배정에 따라 다른 상품으로 바뀐다

화면이 실패했을 때 재시도하거나 에러 페이지로 떨어진다
화면이 실패했을 때 재시도하거나 에러 페이지로 떨어진다

1 / 1

목데이터로 남은 화면들

솔직하게 말하면, 화면 중 일부는 아직 목데이터 위에 서 있습니다. 홈의 시세 그래프는 single-view/_mock.ts의 고정 배열을 그리고, 집이 여러 채일 때의 최근 실거래가 목록과 실거래가 상세 화면은 통째로 하드코딩입니다. API 스펙이 화면 작업보다 늦게 나오는 순서로 진행하다 보니 화면부터 붙였고, 응답을 그래프 데이터로 바꾸는 getMarketPriceGraphData·getActualTransactionGraphData는 다 짜뒀지만 연결 전에 제 몫이 끝났습니다. 데모에서는 숫자가 늘 같아 편했지만, 이건 장점이 아니라 미완입니다.

버그도 하나 남아 있습니다. getMyTransactionGraphData의 switch에 break가 없어 모든 case가 default까지 흘러가고, 결국 amount가 항상 null이 됩니다. 내 거래 점이 그래프에 절대 안 찍힌다는 뜻이에요. 타입 체커가 잡아 주지 못하는 자리였고, ESLint 설정에 no-fallthrough도 react-hooks도 없었습니다. 같은 이유로 DetailView에서 useHistory()가 조건부 return 뒤에 놓인 것도 통과했습니다.

다시 만든다면 두 가지를 바꾸겠습니다. 하나는 목데이터를 화면 파일 안에 두지 않는 것입니다. 이미 테스트에서 msw로 API를 가로채고 있었으니, 같은 fixture를 개발 모드에서도 쓰게 했다면 '연결만 하면 되는 상태'와 '하드코딩된 상태'가 섞이지 않았을 거예요. 다른 하나는 계약 타입 분기입니다. 매매·전세·월세를 switch로 세 군데에서 반복하는 대신 판별 유니온으로 모델링했다면, 위의 fallthrough는 애초에 컴파일되지 않았을 겁니다.

Read next

제휴 금융사 확정 금리·한도 비교 서비스

Finset — 2020