장진호Peter Jang

Peter Jang

Product Engineer

기간Period2025.04 ~ 현재Apr 2025 – Present

역할RoleProduct Engineer

회원 및 인증체계 개편Revamped the membership and authentication system

컬리 회원만 쓸 수 있던 루션에 구글, 애플, 카카오, 네이버 로그인을 붙였습니다.

I added Google, Apple, Kakao, and Naver sign-in to Roution, which had been open only to Kurly members.

서버가 인증 방식을 컬리 게이트웨이에서 루션 자체 토큰(JWT)으로 바꾸는 데 맞춰, 앱과 웹뷰, 만보기 백그라운드 프로세스의 로그인과 토큰 관리를 새로 설계하고 구현했습니다.

As the server moved authentication from the Kurly gateway to Roution’s own tokens (JWT), I redesigned and implemented sign-in and token management across the app, the WebView, and the pedometer background process.

그림 1Figure 1인증 구조: 로그인할 때만 외부 인증을 거칩니다Auth architecture: external authentication only at sign-in소셜 로그인과 컬리 로그인은 처음 한 번만 외부 인증을 거치고, 그 뒤 모든 요청은 루션 서버가 발급한 JWT 하나로 인증합니다. 전환 전 앱은 게이트웨이가 컬리 세션을 확인하는 기존 경로로 계속 동작합니다.Social and Kurly sign-in go through external authentication only once. After that, every request is authenticated with a single JWT issued by the Roution server. Pre-migration app versions keep working through the existing path, where the gateway checks the Kurly session.
로그인 수단구글JWKS 검증애플JWKS 검증카카오userinfo네이버userinfo컬리SSO 교환루션 인프라루션 앱루션 JWT구버전 앱컬리 세션게이트웨이Istio, 플랫폼 공용신규 API 도메인토큰을 그대로 전달기존 API 도메인컬리 세션 검증루션 API 서버EKSJWT 발급과 검증access, refresh 토큰Redisrefresh 토큰, OTPMySQL로그인 수단 1:NSecrets ManagerJWT 서명 키컬리 회원 서버컬리 토큰 검증컬리 쿠폰 API컬리 연동 회원만SMS 발송 서비스SMS 인증번호SDK 로그인서명 검증, userinfo Sign-in methodsGoogleJWKSAppleJWKSKakaouserinfoNaveruserinfoKurlySSORoution infrastructureRoution appRoution JWTLegacy appKurly sessionGatewayShared IstioNew API domainPasses token throughLegacy API domainChecks Kurly sessionAPI serverEKSIssues, verifies JWTsAccess, refresh tokensRedisRefresh tokens, OTPMySQLSign-in methods 1:NSecrets ManagerJWT signing keyKurly membersKurly token checkKurly coupon APIKurly-linked onlySMS serviceVerification codesSDK sign-inSignature, userinfo
실선: 루션 JWT 경로Solid: Roution JWT path 점선: 전환 전 앱 경로Dashed: Pre-migration app path 회색 실선: 로그인과 서버의 외부 호출Gray solid: Sign-in and server-side external calls

요소를 가리키면 설명이 보입니다.

Hover over an element to see details.

그림 2Figure 2앱 안의 토큰 관리: 갱신을 한 곳으로 모았습니다Token management in the app: refresh happens in one place앱 화면과 웹뷰, Android 만보기 백그라운드 프로세스가 같은 로그인 토큰을 씁니다. 여러 곳이 따로 갱신하면 토큰이 엇갈려 로그아웃될 수 있어서, 갱신은 RoutionAPI 한 곳에서 한 번에 하나씩 처리합니다. 만보기 프로세스는 갱신을 앱에 맡기고, 15초 안에 응답이 없을 때만 직접 갱신합니다.App screens, the WebView, and the Android pedometer background process share the same sign-in token. If each refreshed on its own, tokens could fall out of sync and log the user out, so RoutionAPI handles every refresh in one place, one at a time. The pedometer process leaves refreshing to the app and refreshes directly only if there is no response within 15 seconds.
외부 인증소셜 4종, 컬리 SSO 루션 API 서버access, refresh 토큰 발급 인증 UseCase로그인, 가입, 계정 연동 RoutionAPI갱신은 하나씩, 401이면 갱신 후 재시도 세션 저장소토큰, 가입 진행 상태 앱 메인 프로세스 루션 웹 (웹뷰)앱이 넣어 준 토큰 사용 Android 만보기갱신은 앱에 맡김, 15초 뒤 직접 소셜 SDK, 컬리 SSO 인증 헤더 직접 갱신 토큰 전달 토큰 전달 갱신 요청 External auth4 social providers, Kurly SSO Roution API serverIssues access and refresh tokens Auth UseCaseSign-in, sign-up, account linking RoutionAPIOne refresh at a time, retry on 401 Session storeTokens, sign-up progress App main process Roution web (WebView)Uses the token the app injects Android pedometerDefers to the app, 15s fallback Social SDKs, Kurly SSO Auth header Direct refresh Passes token Passes token Asks to refresh

자체 OTA 배포 시스템 설계Designed an in-house OTA deployment system

앱스토어 심사 없이 JS 번들을 바로 내려보내는 OTA 업데이트를, 외부 유료 서비스(EAS Update) 대신 직접 설계한 인프라로 옮겼습니다.

I moved OTA updates, which ship JS bundles straight to users without app store review, off a paid third-party service (EAS Update) and onto infrastructure I designed.

서버 프로그램 없이 S3와 CloudFront 엣지 함수로 동작하고, 어드민에서 기기 단위로 조금씩 배포하고 되돌릴 수 있습니다.

It runs on S3 and CloudFront edge functions with no server application, and the admin can roll out gradually device by device, or roll back.

그림 1Figure 1인프라 구성: 파일을 올리는 곳과 사용자에게 보내는 곳을 나눴습니다Infrastructure: uploading files is separate from serving themGitHub Actions는 서명한 번들을 S3에 추가만 하고, 어떤 기기에 어떤 번들을 줄지는 어드민이 KeyValueStore의 포인터로 정합니다. 앱이 업데이트를 확인하면 CloudFront 엣지 함수가 이 포인터를 읽어 줄 파일을 고릅니다. 운영과 스테이징에 같은 구성을 한 벌씩 둡니다.GitHub Actions only adds signed bundles to S3. The admin decides which device gets which bundle through pointers in KeyValueStore. When the app checks for an update, a CloudFront edge function reads these pointers to pick the file to serve. Production and staging each get an identical copy of this setup.
AWS, 환경마다 한 벌GitHub Actions판정, export, 서명루션 어드민지정 기기, 비율, 롤백S3 버킷추가만 허용KeyValueStore안정, 후보, 비율CloudFront엣지 캐시 끔헤더 함수응답 헤더라우팅 함수파일 고르기Akamai캐시 없이 전달루션 앱expo-updates추가쓰기읽기조회원본 요청업데이트 확인 AWS, one per environmentGitHub ActionsDecide, export, signRoution adminDevices, %, rollbackS3 bucketAppend-onlyKeyValueStoreStable, candidate, %CloudFrontEdge cache offHeader fnSets headersRouting fnPicks the fileAkamaiForwards, no cacheRoution appexpo-updatesAddWriteReadLookupTo originUpdate check
발행: GitHub Actions가 파일을 추가Publish: GitHub Actions adds files 제어: 어드민이 포인터를 바꿈Control: the admin changes pointers 읽기: 앱이 받아 감Read: the app downloads

요소를 가리키면 설정이 보입니다.

Hover over an element to see its configuration.

그림 2Figure 2배포 흐름: 릴리스 하나가 사용자에게 닿기까지Deployment flow: how a release reaches users릴리스마다 같은 네이티브의 스토어 빌드가 이미 있는지 봅니다. 없으면 스토어 빌드를 만들고, 있으면 네이티브 코드의 지문(fingerprint)이 그 빌드와 같은지 확인한 뒤 OTA 후보로 발행합니다. 후보는 발행만으로는 사용자에게 닿지 않고, 어드민에서 지정 기기부터 넓혀 갑니다.For each release, the pipeline checks whether a store build with the same native code already exists. If not, it makes a store build. If so, it confirms the native code’s fingerprint matches that build, then publishes an OTA candidate. Publishing alone doesn’t reach users; the admin widens the rollout, starting with designated devices.
릴리스 태그스토어 빌드가 있나fingerprint가 같나후보 발행서명, 승인 뒤 S3에 추가스토어 빌드Xcode Cloud, Android 러너발행 중단네이티브 변경이 섞임지정 기기사내 테스터 먼저비율 확대받은 기기는 그대로전체 배포안정 번들을 교체롤백직전 안정 번들로있음같음없음다름어드민에서 시작어느 단계든 Release tagStore build exists?Same fingerprint?Publish candidateSign, approve, add to S3Store buildXcode Cloud, Android runnerPublish blockedNative changes foundDesignated devicesInternal testers firstRaise the percentageDevices that have it keep itFull rolloutReplaces the stable bundleRollbackBack to last stableYesYesNoNoStarted in adminAny stage

요소를 가리키면 설명이 보입니다.

Hover over an element to see details.

그림 3Figure 3점진적 배포: 새 번들을 기기 단위로 조금씩 넓힙니다Gradual rollout: widening a new bundle device by device어드민에서 지정 기기부터 시작해 비율을 올리고, 전체 배포로 마칩니다. 기기마다 정해진 값이 비율 안에 드는 기기만 새 번들을 받으므로, 비율을 올려도 이미 받은 기기는 그대로입니다. 문제가 보이면 어느 단계에서든 롤백합니다.In the admin, a rollout starts with designated devices, raises the percentage, and ends with a full rollout. Only devices whose fixed per-device value falls within the percentage get the new bundle, so raising the percentage never drops devices that already have it. If a problem shows up, it can be rolled back at any stage.
새 번들(후보)New bundle (candidate) 기존 번들(안정)Current bundle (stable) 지정 기기(사내 테스터)Designated devices (internal testers)
새 번들을 받은 기기Devices with the new bundle
0/ 100대/ 100 devices
자동 진행 (어드민 프리셋)Auto-advance (admin presets)
릴리스가 바뀌면 대상 기기도 다시 섞입니다A new release reshuffles which devices are included

비율을 움직이거나 자동 진행을 눌러 보세요.

Drag the slider or press an auto-advance preset.

그림 4Figure 4전환 과정: 베타 앱에서 먼저 검증하고 운영 앱으로 옮기는 중입니다Migration: proven on the beta app first, now moving the production app사용자가 쓰는 앱을 멈추지 않도록, 사내 테스트용 베타 앱과 스테이징 환경에서 차례로 검증했습니다.To keep the app people use running without interruption, I validated each step in turn on the internal beta app and in staging.
  1. 0빌드 환경의 지문 통일Unify fingerprints across build environments완료Done
  2. 1베타 앱에 시범 적용Pilot on the beta app완료Done
  3. 2-1스테이징에서 점진적 배포 검증Validate gradual rollout in staging완료Done
  4. 2-2베타 앱을 새 인프라로 이전Move the beta app to the new infrastructure완료Done
  5. 3운영 앱 전환Switch the production app진행 중In progress
  6. 4기존 유료 서비스(EAS) 정리Retire the paid service (EAS)예정Planned

AI 개발 워크플로우 구축Built an AI development workflow

요구사항을 넣으면 스펙 작성부터 테스트 계획, 설계, 구현, 코드 리뷰, 테스트까지 AI 세션이 이어서 진행하는 개발 파이프라인을 만들었습니다.

I built a development pipeline in which, given the requirements, AI sessions carry the work from spec writing through test planning, design, implementation, code review, and testing.

사람의 개입은 최소로 줄이면서 신뢰할 수 있는 결과물을 만들고, 토큰을 효율적으로 쓰는 것이 목표입니다.

The goal is reliable output with minimal human intervention and efficient token use.

설계 의도Design intent

단계마다 새 세션A fresh session for every step
컨텍스트 오염에서 완전히 자유로워지기 위해서입니다.So no step is ever affected by context pollution.
고칠 게 없을 때까지 검토Review until nothing is left to fix
완전히 명확한 요구사항을 넘겨, 결과물이 의도에서 벗어나지 않게 합니다.Handing off fully unambiguous requirements keeps the output from drifting from the intent.
PICT로 테스트 조합 생성Test combinations generated with PICT
AI가 떠올리기 어려운 복잡한 케이스를 결정론적으로, 놓치지 않고 테스트합니다.Complex cases that AI would struggle to think of are tested deterministically, with none missed.
그림 1Figure 1파이프라인 전체: 30단계 중 사람이 확인하는 곳은 두 곳입니다The full pipeline: a human checks in at only 2 of 30 steps채운 칸은 AI 세션, 테두리만 있는 칸은 스크립트 검사, 마름모는 사람이 확인하는 곳입니다.Filled pills are AI sessions, outlined pills are script checks, and diamonds are human checkpoints.
1스펙요구사항, Figma, UX 역할을 나눈 에이전트 팀이 작성2테스트 계획조합과 시나리오, 두 갈래3설계컴파일되는 .ts 계약까지4구현레이어 4개를 동시에, 통합에서 전체 compile과 lint5검증요구사항 추적과 코드 리뷰6테스트기존 테스트와 웹 e2e7보류 구간사람이 기능을 다듬은 뒤 앱 e2e를 따로 실행사람이 확인스펙과 검토 리포트를 보고 다음 단계로사람이 확인두 갈래가 끝나면 확인, 지금은 자동 통과스펙 작성스펙 검토파라미터 추출파라미터 검토조합 생성추적성 확인시나리오 추출시나리오 검토커버리지 확인E2E 조합 선정E2E 계획 작성E2E 계획 검토설계와 .ts 계약설계 검토도메인데이터UI웹통합@covers 확인체크리스트 생성코드 리뷰기존 테스트웹 e2e테스트 수정통과할 때까지계획 맞춤Detox 작성Detox 실행 1SpecWritten by an agent team split into requirements, Figma, and UX roles2Test planTwo tracks: combinations and scenarios3DesignDown to compilable .ts contracts4ImplementationFour layers in parallel, then a full compile and lint at integration5VerificationRequirement tracing and code review6TestingExisting tests and web e2e7On holdApp e2e runs separately after a person polishes the featureHuman checkRead the spec and review report, then move onHuman checkCheck once both tracks finish; auto-passes for nowWrite specReview specExtract paramsReview paramsPICT combosTraceabilityFind scenariosScenario reviewCoverage checkE2E combosWrite E2E planReview E2E planDesign + .tsReview designDomainDataUIWebIntegrateCheck @coversMake checklistCode reviewExisting testsWeb e2eFix testsuntil it passesReconcile planWrite DetoxRun Detox
AI 세션 19 스크립트 게이트 9 사람 확인 2 고칠 게 없을 때까지 새 세션으로 다시 검토, 최대 15회
19 AI sessions 9 script gates 2 human checks Re-review in a fresh session until nothing is left to fix, up to 15 times

단계를 가리키면 하는 일이 보입니다.

Hover over a step to see what it does.

앱 소프트웨어 아키텍처 설계Designed the app’s software architecture

온보딩부터 식단 기록과 사진 분석, 미션과 리워드, 만보기까지 사용자가 매일 쓰는 앱의 핵심 흐름을 만들었습니다.

I built the core flows people use every day, from onboarding to meal logging and photo analysis, missions and rewards, and the pedometer.

모든 화면은 이 레이어 구조 위에서 구현했습니다.

I built every screen on top of this layered architecture.

그림 1Figure 1앱 구조: 요청은 아래 레이어로, 화면 이동은 이벤트로App architecture: requests flow down the layers, navigation goes through events화면(View)은 그리기만 하고, 상태와 동작은 ViewModel이, 데이터 요청은 UseCase와 Repository가 맡습니다. ViewModel은 화면을 직접 옮기지 않고 이벤트를 발행하며, 화면이 이를 구독해 이동합니다.The View only renders. The ViewModel owns state and actions, and UseCases and Repositories handle data requests. The ViewModel never navigates directly; it publishes events, and the View subscribes to them to navigate.
RouteuseAppRouter, 중복 push 차단 View그리기만 담당 ViewModel상태, 액션, Event 발행 UseCaseexecute() 하나, DTO를 모델로 RepositoryDTO 반환, OS별 파일 RoutionAPI토큰, 401 갱신 EventPublisherViewModel이 발행View가 구독해 이동 Store (zustand)전역: DI 상수, persist기능: 화면 간 작성 상태 루션 API 서버 DI Containerinversify로 ViewModel, UseCase, Repository에 주입 구독 발행 갱신 RouteuseAppRouter, blocks duplicate pushes ViewRendering only ViewModelState, actions, publishes Events UseCaseA single execute(), DTO to model RepositoryReturns DTOs, per-OS files RoutionAPITokens, refresh on 401 EventPublisherViewModel publishesView subscribes and navigates Store (zustand)Global: DI constants, persistFeature: drafts across screens Roution API server DI ContainerInjected into ViewModels, UseCases, and Repositories with inversify Listen Publish Update

요기요

기간Period2022.05 ~ 2024.09May 2022 – Sep 2024

역할RoleiOS Developer / Platform Engineer

Server-driven UI 구조 설계 및 개발 리드Led the design and development of server-driven UI

네이티브 앱은 UI 하나를 고치는 데도 앱 업데이트가 필요하고, 업데이트는 사용자에게 늦게 닿습니다. 그래서 서버가 내려 주는 데이터에 따라 UI를 자유롭게 바꿀 수 있는 구조를 만들었습니다.

In a native app, even a small UI change needs an app update, and updates reach users slowly. So I built an architecture that lets the UI change freely based on data sent by the server.

한 화면을 컴포넌트 단위로 쪼개고, 서버가 컴포넌트 정보를 내려 주면 앱은 그 정보대로 그립니다.

Each screen is split into components. The server sends the component data, and the app renders exactly that.

풀어야 했던 문제Problems to solve

성능과 코드 구조Performance and code structure
이전에 시도된 Server Driven UI는 성능이 느리고 코드 구조를 이해하기 어려웠습니다.An earlier attempt at server-driven UI was slow, and its code was hard to understand.
로직 없이 기능 구현Features without logic
앱은 서버가 준 데이터대로 그리기만 하므로, 뷰에 로직을 직접 넣지 않고 기능을 구현해야 했습니다.Since the app only renders what the server sends, features had to work without putting logic directly into views.
동적 범위 조절Scoping what is dynamic
네이티브 앱을 HTML처럼 전부 동적으로 그리기에는 리소스가 부족했습니다. 바꿀 수 있는 범위를 적정 수준으로 제한하면서도 범용성을 지켜야 했습니다.There weren’t enough resources to make a native app fully dynamic like HTML. The changeable scope had to be limited to a sensible level while staying general-purpose.
그림 1Figure 1화면 구성: 한 화면을 컴포넌트로 나눕니다Screen composition: one screen, split into components서버는 화면을 이루는 컴포넌트의 정보를 내려 주고, 앱은 그 정보대로 컴포넌트를 그립니다. 파란 상자 하나가 컴포넌트 하나입니다.The server sends data for the components that make up a screen, and the app renders each component from it. Each blue box is one component.
서버가 내려 주는 정보탭 메뉴카테고리정렬과 필터가게 목록 Data from the serverTab menuCategoriesSort and filterRestaurants
그림 2Figure 2성능: 바뀐 셀만 다시 그립니다Performance: redraw only the cells that changed데이터가 조금만 바뀌어도 화면 전체를 다시 그리던 것을, 바뀐 셀만 다시 그리도록 바꿨습니다. 그 과정에서 생긴 애니메이션 문제는 직접 만든 DataSource로 풀었고, 컴포넌트 데이터는 병렬로 불러옵니다.The whole screen used to redraw on even a small data change; I changed it so only the changed cells redraw. I solved the animation issues this introduced with a custom DataSource, and component data now loads in parallel.
성능이 느리다원인데이터 일부만 바뀌어도화면 전체를다시 그린다시도Diffable DataSource로바뀐 셀만 다시 그려불필요한 렌더링을 줄인다새 문제원치 않는 셀 재사용으로애니메이션 구현이어려워졌다결과이미 화면에 보이는 셀은다른 객체로 바꾸지 않고그 셀 안에서 갱신하는Custom DataSource결과각 컴포넌트 데이터를 병렬로 불러온다 SlowperformanceCauseA small data changeredraws thewhole screenAttemptDiffable DataSourceredraws only changedcells, cutting needlessrenderingNew problemUnwanted cell reusemade animationshard to implementResultCustom DataSourceupdates visible cellsin place instead ofswapping in new objectsResultEach component’s data loads in parallel
그림 3Figure 3구조: 흐름은 한 방향으로, 기능은 딥링크로Architecture: one-way flow, features as deep links뷰와 소통하던 Delegate가 얽혀 코드를 이해하기 어려웠고, 서버가 그리는 화면에는 로직을 넣을 수도 없었습니다. 레이어를 나눠 흐름을 한 방향으로 만들고, 기능은 딥링크로 미리 정의해 서버가 호출하게 했습니다.Tangled Delegates for talking to views made the code hard to follow, and server-rendered screens couldn’t hold logic. I separated the layers so everything flows one way, and predefined features as deep links for the server to call.
코드 구조를이해하기 어렵다원인뷰와 소통하려고 쓴 Delegate가수없이 얽혀 있다아이디어레이어를 명확히 나누고호출과 데이터가한 방향으로만 흐르게 한다로직을 넣지 않고기능을 구현해야 한다아이디어기능을 딥링크 형태로 미리 정의하고, 서버가 이를 호출하게 한다.딥링크는 두 가지 원칙을 따른다.1. 딥링크끼리는 서로 의존하지 않는다.2. 딥링크의 동작은 넘겨받은 파라미터로만 정해진다.그래서 딥링크는 순수함수 같은 형태가 된다.결과단방향 구조UI 모듈은 모델을 받아뷰를 그리되 세부 사항은드러내지 않는다UI 레이어는 로직을 모른 채딥링크를 호출해기능을 동작시킨다 Code structureis hard to followCauseDelegates used to talk to viewsare tangled everywhereIdeaSeparate the layers clearlyso calls and data flowin one direction onlyFeatures mustwork withoutadding logicIdeaPredefine features as deep links and let the server call them.Deep links follow two rules:1. Deep links never depend on each other.2. A deep link’s behavior depends only on its parameters.So each deep link works like a pure function.ResultOne-way structureUI modules take a modeland draw views withoutexposing detailsThe UI layer triggersfeatures via deep linkswithout knowing logic
그림 4Figure 4동적 범위: 기술이 아니라 합의로 정했습니다Dynamic scope: settled by agreement, not technology서버가 어디까지 바꿀 수 있게 할지는 기술만으로 정할 수 없었습니다.How much the server should be able to change couldn’t be decided by technology alone.
컴포넌트의 동적 기능 범위를적정 수준으로 조절해야 한다결과기술만으로는 이 문제를 풀 수 없다개발자, 디자이너, PO, 운영 담당자와 계속 소통해 스펙을 정했다 Set the right scope fordynamic component featuresResultTechnology alone can’t solve thisSet the spec through ongoing talks with developers, designers, POs, and ops

Tuist를 활용한 프로젝트 구조 및 성능 향상Improved project structure and performance using Tuist

빌드 시간이 너무 오래 걸렸고, 함께 일하는 사람은 늘어나는데 프로젝트는 그만큼 확장하기 어려웠습니다. 프로젝트 파일에서는 충돌도 자주 났습니다.

Builds took far too long, and while the team kept growing, the project couldn’t scale with it. Project files also caused frequent conflicts.

Tuist를 적용해 프로젝트 구조를 바꿨습니다. 적용 과정은 요기요 기술 블로그에 정리했습니다.

I restructured the project with Tuist and wrote up the process on the Yogiyo tech blog.

iOS 프로젝트에 Tuist 적용기Applying Tuist to an iOS project (Korean)techblog.yogiyo.co.kr

그림 1Figure 1모듈 구조: 기능마다 UI, Domain, Data 모듈을 둡니다Module structure: UI, Domain, and Data modules for each feature기술 블로그 글에 실은 구조입니다. 홈, 검색, 가게 같은 기능이 각각 UI, Domain, Data 모듈을 가집니다.This is the structure from the blog post. Each feature, such as Home, Search, and Restaurant, has its own UI, Domain, and Data modules.
FeaturesHomeUIDomainDataSearchUIDomainDataRestaurantUIDomainData다른 기능 FeaturesHomeUIDomainDataSearchUIDomainDataRestaurantUIDomainDataOther features
그림 2Figure 2결과: 빌드와 실행이 빨라지고 충돌이 사라졌습니다Results: faster builds and launches, and no more conflicts
CI/CD 빌드 시간 감소reduction in CI/CD build time
88%
콜드 런치 시간 감소reduction in cold launch time
50%
프로젝트 파일로 인한 코드 충돌code conflicts caused by project files
없음None

로그 데이터 전송 효율화 설계Designed more efficient log data transmission

로그를 이벤트가 생길 때마다 하나씩 보내다 보니, 기기 한 대가 1초에 약 10번 요청을 보내 트래픽이 과도했습니다.

Logs were sent one by one as each event occurred, so a single device made about 10 requests per second, creating excessive traffic.

여러 로그를 묶어 보내 트래픽을 줄이고, 바로 보내지 않는 동안 로그가 사라지지 않도록 스토리지에도 캐싱했습니다.

I batched logs to cut the traffic, and cached them in storage too so they wouldn’t be lost while waiting to be sent.

그림 1Figure 1전송 방식: 모아서 보내고, 못 보낸 로그는 다음 실행 때 보냅니다Transmission: send in batches, and send leftover logs on the next launch묶어 보내려고 이벤트를 잠시 모아 두면, 그사이 앱이 종료될 때 로그가 사라집니다. 그래서 모아 둔 이벤트를 메모리뿐 아니라 스토리지에도 저장합니다.Holding events briefly to batch them means logs are lost if the app quits in the meantime. So the pending events are saved to storage as well as memory.
로그 전송트래픽이과도하다원인수많은 이벤트가발생 즉시 하나씩전송된다시도여러 이벤트를 묶어한 번에 전송하고이벤트 pool로 관리한다새 문제pool에 들어간 채로앱이 종료되면로그가 누락된다결과pool을 메모리뿐 아니라스토리지에도 저장해다음 실행 때 보낸다 Too muchlog trafficCauseMany events are sentone by one, rightas they occurAttemptBatch multiple eventsinto one request, heldin an event poolNew problemLogs in the poolare lost if theapp quitsResultSave the pool to storageas well as memory, andsend it on next launch