팀이 커질수록 빌드는 느려지고, 의존성은 뒤엉킨다. 새 기능 하나를 추가하려면 관련 없는 모듈까지 전부 컴파일해야 했고, 로컬 빌드는 어느새 커피 한 잔을 넘어 점심시간을 잡아먹는 일이 되어 있었다. 문제는 코드가 아니라 구조였다.
모듈 경계를 다시 그리는 작업부터 시작했다. 기능 단위로 책임을 쪼개고, Tuist로 의존성 그래프를 명시적으로 선언해 "누가 누구를 알아야 하는가"를 코드가 아닌 설계 문서 수준에서 강제했다. TCA와 SwiftUI를 표준 패턴으로 도입한 것도 같은 이유였다 — 새로 합류한 팀원이 기존 관습을 추측하지 않고, 구조를 보면 규칙을 알 수 있도록.
"빠른 빌드"가 목표가 아니라, 팀이 매번 같은 실수를 반복하지 않는 구조가 목표였다.
이 구조 전환은 이후 CI/CD를 수동 배포에서 Jenkins, 그리고 GitHub Actions로 이전하는 작업의 기반이 되었다.
이 구조는 한 번에 전체로 확장하지 않았다. 2023년부터 카테고리, 기획전, 설정 등 여러 화면에 먼저 적용해 패턴을 검증했고, 그렇게 쌓은 확신을 바탕으로 2025년 리브랜딩에서 서비스의 가장 중요한 지면인 홈 화면 전체를 SwiftUI와 TCA로 다시 짰다. 자체 UI 프레임워크의 유지보수 부담, 비즈니스 로직과 화면 상태 추적의 어려움, 시니어와 주니어 사이의 생산성 격차 — 오래 쌓인 문제를 한 번에 풀려 하지 않고, 이미 검증된 패턴을 넓혀가는 방식으로 풀었다.
새 아키텍처를 도입할 때 가장 쉬운 길은 정답을 정해서 통보하는 것이다. 하지만 그렇게 자리잡은 규칙은 그걸 정한 사람이 떠나는 순간 함께 흔들린다. TCA 도입을 검토할 때도 러닝커브를 이유로 신중한 입장이었지만, 이번만큼은 결론에 도달하는 방식 자체를 다르게 가져가고 싶었다.
정답을 제시하는 대신 팀원들과 함께 같은 기능을 여러 구조로 구현해보고, 장단점을 나란히 놓고 비교했다. MVVM+Combine과 TCA로 각각 프로토타입을 만들어본 뒤, 장기적인 유지보수 관점에서 어떤 구조가 팀에 남을지 함께 판단했다. TCA로 결론이 났지만, 결론 자체보다 거기 도달한 과정이 이후 팀의 의사결정 방식을 바꿔놓았다.
"정답을 이야기하기보다 방향을 이야기하려고 합니다. 방향은 제시하지만 구현 방식은 팀이 스스로 결정하도록 합니다."
같은 원칙은 디자인 시스템에도 적용했다. Dynamic Type 지원을 제안했을 때 디자인이 무너질 거라는 우려가 있었지만, 실제로 큰 글꼴을 쓰는 사용자가 생각보다 많다는 데이터를 공유했고 대부분의 화면에 자연스럽게 반영됐다. 이후로는 새 디자인 요구사항이 생기면 시스템이 먼저 막지 않고, 기능을 먼저 구현한 뒤 시스템에 편입하는 방식으로 운영을 바꿨다.
통합 로그인의 OAuth 토큰 관리 구조를 개선하는 프로젝트였다. 레거시 코드와의 호환성을 지키려 애썼지만, 실제 운영 환경은 예상과 다르게 무너졌다 — 여러 경로에서 동시에 토큰을 갱신하며 Race Condition이 발생했고, 일부 케이스는 Deadlock에 가까운 상태로 이어졌다. 커리어에서 가장 크게 실패한 순간이었다.
문제는 기술이 아니라 그 기술을 검증한 방식이었다. 단위 테스트는 충분히 통과했지만, 실제 서비스의 동시 요청 패턴까지는 검증하지 못했다. 실패를 인정한 뒤 Swift Concurrency와 Actor 모델을 처음부터 다시 공부했고, 공유 가변 상태를 격리하는 구조로 다시 설계했다.
"이 경험 이후에는 새로운 기술을 도입할 때 테스트 코드뿐 아니라 실제 서비스 환경에서 어떤 문제가 발생할 수 있는지 더 많이 고민하게 되었습니다."
지금도 이 구조를 Actor 기반으로 완전히 전환하는 작업을 계속 제안하고 있다. 실패 자체보다, 그 실패를 인정하고 다시 배우는 태도가 이후 팀에서 새 기술을 검토하는 기준이 됐다.
검색 결과, 상품 상세, 프로모션 페이지 — 해외숙소 서비스는 오랫동안 조직 단위로 쪼개져 있었다. 각 팀은 자기 화면을 잘 만들었지만, 사용자에게는 그저 하나의 여행이었다. 화면과 화면 사이 이동은 매끄럽지 않았고, 같은 정보가 스크린마다 다르게 표현됐다.
리메이크를 맡으면서 가장 먼저 한 일은 화면 목록이 아니라 사용자 시나리오를 그리는 것이었다. 사용자가 실제로 밟는 흐름을 따라, 각 조직이 독립적으로 개발하던 화면들을 하나의 여정으로 다시 연결했다. 조직 구조를 존중하면서도, 판단 기준을 "우리 팀 화면"에서 "사용자의 다음 걸음"으로 바꾸는 작업이었다.
"기능 하나를 개발하더라도 사용자가 어떤 흐름으로 서비스를 이용하는지 먼저 생각하고, 사용자가 더 좋은 경험을 할 수 있는 방향이라면 그 선택을 우선합니다."
이 프로젝트 이후 새 기능을 검토할 때 "이 화면이 필요한가"보다 "사용자의 여정에서 이 지점이 필요한가"를 먼저 묻는 습관이 팀에 남았다.
Jenkins 중심의 CI 환경을 GitHub Actions로 옮기기로 했을 때, 처음부터 완벽한 파이프라인을 그리지 않았다. 대신 가장 작은 단위부터 시작했다 — 기존 Fastlane 배포 스크립트를 그대로 GitHub Runner 위에서 실행하는 것. 바뀐 건 실행 위치뿐, 팀은 아무것도 새로 배우지 않아도 됐다.
그 위에 Lint, Formatter, Unit Test, AI Code Review를 하나씩 얹었다. 각 단계는 이전 단계가 문제없이 자리잡은 걸 확인한 뒤에만 추가했다. 한 번에 이상적인 파이프라인을 설계하려 했다면 도입 자체가 늦어졌을 것이다. 지금은 Slack 알림까지 포함한 자동화된 리뷰 환경으로 확장됐고, 여전히 계속 다듬는 중이다.
"먼저 동작하는 것을 만들고, 실제 사용하면서 점진적으로 개선하는 방식을 선호합니다."
같은 습관은 새로운 기술을 도입할 때도 이어졌다. 처음부터 전체 그림을 완성하기보다, 먼저 동작하는 여러 방식을 비교해보고 작은 성공을 반복하며 범위를 넓혀가는 쪽을 택했다.
도전은 사무실 밖에서부터 시작됐다. 상하이의 아시아 헤드쿼터에는 세계 각지에서 모인 개발자들이 있었고, 언어도 배경도 개발 스타일도 제각각이었다. 익숙한 환경을 떠나, 다시 처음부터 실력으로 증명해야 하는 자리였다.
WebRTC 기반 화상 수업 앱을 만드는 프로젝트였다. 런칭은 끝이 아니라 시작이었다 — 그 이후로도 기능은 멈추지 않고 계속 업데이트됐고, 애자일 방식으로 팀과 호흡을 맞추며 매 스프린트마다 실제 사용자의 반응을 반영했다. 국적도 일하는 방식도 다른 동료들과 같은 속도로 나아가려면, 말이 아니라 결과로 신뢰를 쌓는 수밖에 없었다.
실력으로 증명하면, 국적도 언어도 문제가 되지 않는다.
낯선 팀에서 자리를 증명하는 경험은, 이후 어떤 조직에 가더라도 규칙이 아니라 결과로 신뢰를 쌓는 태도로 남았다.
의존성 관리자를 CocoaPods에서 Swift Package Manager로 옮기는 작업은 도구 교체처럼 보이지만, 실제로는 "누구의 코드인가"라는 질문을 지우는 작업이었다. 라이브러리마다 버전 관리 방식이 달랐고, 파일을 열어보면 어느 팀원이 짰는지 스타일만으로 짐작이 갈 정도였다.
SwiftLint와 Formatter를 도입하면서 규칙은 한 사람이 정하지 않았다. 팀원들과 함께 논의해서 기준을 세웠고, 불편한 지점이 나오면 규칙 자체를 다시 고치면서 팀이 자연스럽게 적응하도록 했다. 지금은 모든 라이브러리를 SPM 기반으로 관리한다. 목표는 개인의 스타일을 지우는 게 아니라, 프로젝트 전체가 하나의 코드처럼 보이게 만드는 것이었다.
"누구의 코드인지 구분하기보다 프로젝트 전체가 하나의 코드처럼 보이는 것을 목표로 했습니다."
규칙을 세우는 과정도 다르지 않았다. 여러 구현 방식을 먼저 비교하고, 팀원들의 의견을 충분히 들은 뒤에야 방향을 정했다.
"새로운 기능을 만드는 것보다, 이미 많은 사용자가 쓰는 서비스를 조금씩 더 좋은 방향으로 바꾸는 과정에서 가장 큰 보람을 느낍니다."
As a team grows, builds slow down and dependencies knot together. Adding one new feature meant recompiling modules that had nothing to do with it, and a local build had quietly outgrown a coffee break — it was eating into lunch. The problem wasn't the code. It was the structure.
I started by redrawing the module boundaries — splitting responsibilities by feature and using Tuist to declare the dependency graph explicitly, so "who needs to know about whom" was enforced by the design itself, not left to convention in code. Adopting TCA and SwiftUI as the standard pattern followed the same logic: a new teammate should be able to read the structure and infer the rules, instead of guessing at habits no one wrote down.
"A fast build" was never the goal — the goal was a structure that stopped the team from repeating the same mistake every time.
That shift in structure became the foundation for later moving CI/CD off manual deploys — first to Jenkins, then to GitHub Actions.
This structure didn't roll out everywhere at once. Starting in 2023, I applied it first to a handful of screens — category, curated collections, settings — to prove out the pattern. That track record was what let us rewrite the home screen, the single most important surface in the app, entirely in SwiftUI and TCA during the 2025 rebrand. An in-house UI framework that was expensive to maintain, business logic and screen state that were hard to trace, a productivity gap between senior and junior engineers — instead of solving years of accumulated problems all at once, I solved them by extending a pattern that had already proven itself.
→ Further reading: NOL Home Rebranding with SwiftUI and TCA (Medium, in Korean)
The easiest path when introducing a new architecture is to hand down the answer and announce it. But a rule installed that way comes apart the moment the person who set it leaves. I'd been cautious about TCA myself, worried about the learning curve — but this time I wanted the process of reaching a conclusion to look different, not just the conclusion.
Instead of handing down an answer, we built the same feature multiple ways with the team and compared the trade-offs side by side. We prototyped it in both MVVM+Combine and TCA, then judged together — from a long-term maintenance standpoint — which structure would actually stick. TCA won, but the process of getting there mattered more than the result: it changed how the team made decisions afterward.
"I try to talk about direction, not answers. I'll point the way, but I let the team decide how to get there."
The same principle carried over to the design system. When I proposed supporting Dynamic Type, people worried it would break the design — but the data told a different story: far more users relied on large text sizes than anyone expected, and it landed cleanly across most screens. Since then, a new design requirement doesn't get blocked by the system first; we build the feature, then fold it into the system.
This project set out to fix how unified login managed OAuth tokens. I worked hard to preserve compatibility with legacy code — but production broke in ways I hadn't planned for. Concurrent refresh calls from multiple paths triggered a race condition, and some paths came close to deadlock. It's the biggest failure of my career.
The problem wasn't the technology. It was how I'd validated it. Unit tests passed cleanly, but they never touched the concurrent request patterns of a live service. Once I owned the failure, I relearned Swift Concurrency and the actor model from the ground up, and redesigned the system to isolate shared mutable state.
"Since then, whenever I adopt something new, I spend a lot more time thinking about what can go wrong in production — not just what the tests cover."
I'm still pushing to move this system fully onto actors. More than the failure itself, owning it and relearning from it became the team's bar for how we evaluate new technology now.
Search results, product detail, promotion pages — the overseas accommodations service had been split along org lines for years. Each team built its own screen well, but to the user it was one trip. Moving between screens wasn't seamless, and the same information looked different depending on which screen you were on.
Taking on the remake, the first thing I did wasn't list screens — it was map the user's actual path through the product. Following that path, I reconnected screens that each org had been building independently into a single journey. It meant respecting the org chart while changing the question we asked from "what does our team's screen need" to "what does the user need next."
"Even for a single feature, I think first about the flow a user takes through the product, and I go with whichever choice serves that experience better."
Since this project, the team asks "does this matter to the user's journey?" before it asks "do we need this screen?" when reviewing new features.
When we decided to move our CI off Jenkins and onto GitHub Actions, I didn't design the ideal pipeline up front. I started with the smallest possible step: running our existing Fastlane deploy script, unchanged, on a GitHub runner. The only thing that changed was where it ran — the team didn't have to learn a thing.
From there I layered in lint, formatter, unit tests, and AI code review, one at a time — each added only once the previous stage had settled without issues. Trying to design the perfect pipeline in one pass would have just delayed adoption. Today it's grown into an automated review setup that includes Slack notifications, and it's still evolving.
"I'd rather ship something that works and improve it as people actually use it."
The same habit carried over to how I adopt new technology in general — compare a few working approaches first, then widen the scope one small win at a time, instead of trying to complete the whole picture up front.
The challenge started before I even reached the office. Shanghai's APAC headquarters brought together developers from all over the world — different languages, different backgrounds, different ways of working. Leaving everything familiar behind, I had to prove myself from zero, on skill alone.
The project was a WebRTC video-classroom app. Launch wasn't the finish line — it was the start. Features kept shipping after that, and working sprint by sprint under agile, we folded in real user feedback every cycle. Keeping pace with colleagues who differed in nationality and working style alike left only one option: build trust through results, not words.
Prove yourself through skill, and neither nationality nor language gets in the way.
Proving myself on an unfamiliar team left a lasting habit: build trust through results, not rules, no matter which organization I join next.
Moving the dependency manager from CocoaPods to Swift Package Manager looked like a tool swap. It was really about erasing the question of whose code this was. Every library managed its versions differently, and you could open a file and guess which teammate wrote it just from the style.
When we introduced SwiftLint and a formatter, no one person set the rules. We worked out the standards together as a team, and whenever a rule caused friction, we revised the rule itself so the team could settle into it naturally. Every library is on SPM now. The goal was never to erase anyone's personal style — it was to make the whole project read as one codebase.
"The goal was never to tell whose code was whose — it was for the whole project to read as one codebase."
Setting the rules followed the same process: compare a few approaches first, hear the team out fully, and only then commit to a direction.
"I get more satisfaction from nudging a service that already has plenty of users toward something better, bit by bit, than from building something new from scratch."