기술 스택
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/reactivex.png&w=720&q=90)
ReactiveX
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/swift.png&w=720&q=90)
Swift
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/alamofire.png&w=720&q=90)
Alamofire
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/githubaction.png&w=720&q=90)
Github Action
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/googlefirebase.png&w=720&q=90)
Google Firebase
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/fastlane.png&w=720&q=90)
Fastlane
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/github.png&w=720&q=90)
Github
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/slack.png&w=720&q=90)
Slack
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/notion.png&w=720&q=90)
Notion
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/jira.png&w=720&q=90)
Jira
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/bitrise.png&w=720&q=90)
Bitrise
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/confluence.png&w=720&q=90)
Confluence
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/kubernetes.png&w=720&q=90)
Kubernetes
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/terraform.png&w=720&q=90)
Terraform
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/argocd.png&w=720&q=90)
Argo CD
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/grpc.png&w=720&q=90)
GRPC
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/nodejs.png&w=720&q=90)
NodeJS
![techstack-logo](https://image.wanted.co.kr/optimize?src=https://static.codenary.co.kr/framework_logo/go.png&w=720&q=90)
Go
더 보기
커뮤니티실 API Design-First 접근방식 정착기
안녕하세요! 커뮤니티실 그룹 플랫폼팀의 서버 개발자 하이디(Heidi)예요 저는 동네에서 일어나는 다양한 온·오프라인 활동을 연결하는 모임 서비스를 만들어 나가고 있어요.커뮤니티실은 사용자에게 연결의 가치를 전하기 위해 도전적이고 다양한 기능들을 빠르게 테스트하는 조직이에요.이 과정에서 서버와 클라이언트 간의 API 인터페이스 조율이 자주 발생하게 되어 팀원 모두가 JSON 관리의 달인이 되고 있단 생각을 하곤 해요 . 그렇다 보니 API 라는 소통 수단은, 구현하는 것 뿐만 아니라 설계하는 것 또한 매우 중요하다는 점을 깨닫게 됐어요. 효율적인 API 설계야말로 원활한 데이터 교환을 위한 초석이 되기 때문이에요.그런 의미에서, 오늘은 커뮤니티실 개발자들이 어떤 방식으로 API를 만들어왔고, 현재는 어떻게 만들고 있는지 소개하는 시간을 가져볼까 해요. 어려운 내용은 아니니, 가벼운 마음으로 재밌게 읽어주세요 Design-First커뮤니티실에서 만들고 있는 서비스는 사용자 경험을 중요하게 여기고 있는 만큼 다양한 화면과 기능으로 이루어져 있어요. 따라서 클라이언트 관점에서의 API 설계가 매우 중요해요. 사용자가 어떤 방식으로 서비스를 경험하게 될지를 고려해 API를 설계해야 하니까요.이런 이유로, 커뮤니티실에서는 API를 설계할 때 자연스럽게 Design-First API 접근 방식을 택하게 됐어요.Design-First 접근 방식이란, API 설계 시에 명세를 먼저 작성하는 접근법을 뜻해요.서버 구현에 앞서, 클라이언트와 함께 양쪽의 의견이 잘 버무려진 API 명세를 정의한 다음, 이를 기반으로 클라이언트와 서버를 동시에 구현하는 방식으로 API를 만들고 있다고 이해해주시면 되어요. (아마 많은 분께 익숙한 방식일 것 같아요 )커뮤니티실 API 설계 방식 변천사과거 API 설계 방식, 노션2022년 초에는 주로 노션을 통해 API 명세를 작성했어요. 노션은 누구나 쉽게 접근 및 수정을 할 수 있기에, 서버와 클라이언트가 API 명세에 대해 빠르게 논의할 수 있었기 때문이에요.노션을 이용해 API 를 만드는 과정은 아래와 같아요.API 명세를 작성한다.위의 명세에 기반해2 1. 클라이언트에서 Mocking API를 작성하고, 임시로 사용한다.2 2. 서버에서 API를 작성한다.3. 서버에서 API가 완성되면, 클라이언트에서 해당 API를 사용한다.노션을 이용한 API 작성 과정노션은 구성원이 적은 상황에서 서로 가볍고 빠르게 소통하고 수정할 수 있다는 장점이 있었어요. 하지만 점차 동료들이 늘어나고 서비스의 크기가 커지면서 여러 가지 문제점에 직면하게 되었죠. ~ 노션으로 API 명세를 논의해봤다면 왠지 모르게 익숙할 문제점들 ~1️ 노션 API 명세를 보고, 요청/응답 모델을 수동으로 작성해요. 개발 속도의 저하 2️ 유사한 구조를 가진 API가 늘어날수록, 그에 따른 비효율은 더 커졌어요. 비효율적인 개발 3️ API 명세를 만드는 사람마다 방식이 달라요 표준이 없음 4️ 게시글 조회 API 말고, 사용자 정보 조회 API 명세는 어디서 찾아야 해요? 명세의 파편화 5️ 이 명세 최신 맞아요? API 명세 신뢰도 부족 이러한 문제점들을 하나씩 겪게 되면서, 우리 팀에 더 잘 맞는 API 설계 방법은 없을지 고민하기 시작했어요. 그러다가 기존 문제점들을 보완할 수 있는 새로운 방식을 찾게 되었죠. 그건 바로 API 명세 작성을 위한 IDL인 **OpenAPI Specification** 이었어요.OpenAPI Specification, OASOpenAPI Specification, 줄여서 OAS라고 불리는 이 인터페이스 언어는 HTTP API를 정의하고 문서화하기 위한 규격이에요. API의 엔드포인트, 매개변수, 입출력 데이터 형식을 JSON 이나 YAML 형태로 표현할 수 있어요.또한, OAS의 규격에 맞는 API 명세를 작성하면 openapi-generator라는 도구를 통해 API 문서와 다양한 프로그래밍 언어의 클라이언트/서버 코드를 자동으로 생성해줄 수 있어요.OAS API 명세를 기반으로 생성된 API 문서, 각 언어별 서버/클라이언트 코드OAS를 사용하면 노션을 이용한 기존 API 명세 설계 방식의 문제점들이 이렇게 해결돼요.1️ , 2️ 노션 API 명세를 보고, 요청/응답 모델을 수동으로 작성해요. / 유사한 구조를 가진 API가 늘어날수록, 그에 따른 비효율이 커져요. YAML 이나 JSON으로 API 명세를 작성하면 각 요청과 응답에 대한 코드를 생성할 수 있으므로, 수동으로 작성하지 않아도 돼요.3️ API 명세를 만드는 사람마다 방식이 달라요. OAS에서 제안하는 규격대로 API 명세를 작성해야하기 때문에 API를 일관된 형식으로 작성할 수 있어요.4️ 게시글 조회 API 말고, 사용자 정보 조회 API 명세는 어디서 찾아야 해요? 모든 API 명세를 별도 저장소에서 관리하면, 각 명세를 쉽게 찾을 수 있어요.5️ 이 명세 최신 맞아요? API 명세를 통해 코드로 만들어진 요청/응답 모델을 사용하면, 코드와 명세와 강결합되어 항상 최신성이 보장돼요.그뿐만 아니라 OAS는 누구나 쉽게 이해할 수 있는 YAML 이나 JSON으로 API를 정의할 수 있어요. 그래서 누구나(심지어 개발자가 아니더라도) API 명세에 기여할 수 있죠. 이런 점은 클라이언트 중심 API 설계를 중요하게 여기는 커뮤니티실의 개발 방향성과도 잘 맞아떨어진다고 생각했어요.그래서!(두둥) 커뮤니티실은 OAS로 API 명세를 관리하기로 했어요. 현재 API 설계 방식, OAS새로이 OAS를 도입한 결과, API 명세를 만들어가는 과정은 이렇게 달라졌어요.API 명세를 관리할 전용 저장소(GitHub Repository)를 만든다.해당 저장소에 Pull Request 를 열어 API 명세를 정의하고 검토한다.명세가 main 브랜치에 머지되면, GitHub Actions 워크플로우에서 openapi-generator를 실행해 클라이언트/서버 코드를 자동으로 생성한다.클라이언트/서버는 생성된 코드를 사용한다.OAS를 이용한 API 작성 과정이렇게 API 명세를 논의하는 공간(
notion
kubectl create pod 를 실행하면 발생하는 일 kube-apiserver 감사 로그(Audig Log)로 엿보기
kubectl create pod 를 실행하면 발생하는 일 kube-apiserver 감사 로그(Audig Log)로 엿보기안녕하세요, 당근 SRE팀 클러스터 파트에서 일하고 있는 Karin(카린)이에요. 클러스터 파트는 당근 내부의 모든 서비스가 운영되는 쿠버네티스 환경을 설계하고 관리해요. 쿠버네티스의 관측가시성과 서비스 안정성을 확보하기 위해 모니터링 인프라부터 서비스 메시, 보안까지 여러 방면에서 정책과 로드맵을 설계하고 적용해요.쿠버네티스를 처음 배우는 사람이라면 kubectl create namespace, kubectl create -f sample-pod.yaml 명령어를 한 번쯤 실행해보셨을 거예요.파드를 하나 생성했을 때, 쿠버네티스 내부에서 어떤 일이 일어나는지 상상해 본 적 있나요? 파드가 어떤 노드로 갈지 어떻게 알지? 파드의 MAC, IP 주소는 누가 어떻게 부여하는 거지? 컨테이너는 어떻게 만들어지지? 같은 질문들이요. 저는 쿠버네티스를 처음 배우면서부터 운영을 하고 있는 지금까지, 여전히 이 질문에 대한 답을 더 깊게 찾아가고 있는 것 같아요 하지만, kubernetes apiserver audit log(감사 로그)를 살펴보면서 많은 부분을 배울 수 있었어요! kube-apiserver는 모든 컴포넌트의 중심이 되는 api 서버인 만큼 요청에 대한 자세한 기록, 즉 audit log를 기록해요.쿠버네티스의 각각의 컴포넌트가 어떤 일을 하는지 문서로만 보는 것보다, 로그를 통해 누가 어떤 요청을 보내는지 직접 보니까 훨씬 이해하기 쉬웠어요.공식문서에서 설명하는 audit log로 알 수 있는 것들은 아래와 같아요.무슨 일이 일어났는지언제 일어났는지누가 시작했는지누구에게 일어난 일인지어디에서 관측되었는지어디에서 시작되었는지어디로 가는지로그에 대한 건 아래에서 더 자세히 설명할게요 이 글에서는 파드를 생성할 때 쿠버네티스의 각 컴포넌트(특히 컨트롤 플레인)가 어떤 일을 하는지 audit log를 참고하여 살펴보려고 해요. 아래 내용은 쿠버네티스 공식 문서를 참고해서 간단히 정리한 쿠버네티스 컴포넌트의 역할이에요. 이렇게 보면 누가 무슨 일을 하는지 모르겠지 않나요?? 여기서 다 이해하지 못 하는게 당연해요! 그냥 훑고 넘어가 봅시다. 이 글을 다 읽고 이 부분을 다시 한 번 봐주세요 쿠버네티스 컴포넌트 개요 ( https://kubernetes.io/ko/docs/concepts/overview/components/)컨트롤 플레인 컴포넌트: 쿠버네티스 클러스터를 운영하는 중추kube-apiserver : 쿠버네티스 api를 노출하는, 컨트롤 플레인의 프론트엔드 모든 구성요소는 apiserver를 통해 소통해요!etcd: 모든 클러스터 데이터를 저장하는 저장소예요.kube-scheduler: 새로 생성된 파드, 즉 노드가 할당되지 않은 파드를 감지하고 노드를 배정해 주는 역할이에요.kube-controller-manager: 수많은 컨트롤러 프로세스를 실행하는 단일 파이너리로 컴파일된 컨트롤러예요. (컨트롤러 목록 참고)cloud-controller-manager: 클라우드 공급자에 따라 컨트롤 로직을 포함하는 컨트롤러로, 노드(ec2), 라우트(aws-vpc), 서비스(load balancer)와 관련된 다양한 컨트롤러 포함해요.노드 컴포넌트: 노드를 쿠버네티스로서 작동하게 하는 요소kubelet: 클러스터의 각 노드에서 실행되는 에이전트로, 파드 스펙에 맞춰 컨테이너가 건강하게 동작하도록 관리해요.kube-proxy: 클러스터의 각 노드에서 실행되는 네트워크 프록시로, 쿠버네티스의 서비스(Service) 를 구현해요.컨테이너 런타임: 컨테이너 실행을 담당하는 소프트웨어 (containerd, CRI-O 등 모든 CRI(컨테이너 런타임 인터페이스) 구현체를 지원한다.)이 그림이 바로 오늘 설명하려는 내용의 핵심 요약이라 할 수 있어요. 이 내용을 완벽하게 아시는 분이면 다음 글에서 만나뵙겠습니다. 안녕히 가세요 ~ 아직 헷갈리거나 궁금한 게 더 있는 분이라면 아래 글을 쭉 함께해주세요 ️컨트롤 플레인 컴포넌트에 대한 설명을 위에 짧게 써봤는데, 그래서 쟤네가 각각 무슨 일을 하는건데 ?? 뭐가 다른거야? 에 대한 궁금증을 해결하기 위해! 직접 그 과정을 함께 살펴봐요. kubectl create pod 를 실행할 때 컨트롤 플레인에서 발생되는 로그를 쭉 읽어보면 파드가 생성되는 과정을 그려볼 수 있어요.위 그림에서 각 단계에 해당하는 kube-apiserver 감사 로그를 살펴볼게요.사전준비EKS 제어 플레인 로깅 활성화하기aws eks 환경에서는 eks > 클러스터 > 관찰성 탭의 제어 플레인(컨트롤 플레인) 로깅 에서 API서버, 감사, 스케줄러 로그를 켜주셔야 cloudwatch에서 확인이 가능해요.샘플 파드(pod) 생성하기우선, 테스트를 위한 파드 명세를 준비해요. 저는 공식 문서에 있는 가장 간단한 샘플을 이용했어요. (출처)저는 AWS EKS의 1.28 버전을 사용했는데, audit log 내용이나 파드 명세는 클러스터 버전에 따라 살짝 다를 수 있어요.apiVersion: v1kind: Podmetadata: name: nginxspec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80kubectl create -f pod.yaml -n test 를 실행하니 1초도 안 걸려서 파드가 생성되었어요.kubectl get pod -n test 명령어로 조회해보면 아래와 같이 방금 생성된 파드에 대한 상세 내용을 볼 수 있어요.(여기서 잠깐! 제가 만든 건 nginx 컨테이너 하나만 있는 파드인데, 얘네는 왜 이렇게 복잡하죠 라는 생각이 들텐데, 쿠버네티스에서는 파드에 필요한 default 요소를 미리 기본 스펙으로 준비해 두고, 내가 제공한 명세의 내용을 패치(patch) 해주는 식으로 파드 스펙을 완성해요. 고로, 아래에 나오는 저 복잡한 것들을 필요에 따라 바꿔서 적용할 수 있어요.)# kubectl get pod -n test -o yam
kubernetes
nodejs
업무에 손쉽게 Golang 적용하기: 로케이션 코어팀 백엔드 개발자가 일하는 방식
안녕하세요! 로케이션 코어팀의 백엔드 개발자 샐리예요.로케이션 코어팀은 당근이 하이퍼로컬 앱으로 나아가는 데 기반이 되는 위치 기반 서비스와 지도 플랫폼을 개발하고 운영하는 팀이에요. 저는 팀 내에서 지도를 안정적으로 제공할 수 있는 파이프라인을 구축하고, 리소스를 관리할 수 있는 플랫폼을 개발하고 있어요.당근은 팀마다 서로 다른 기술 스택을 사용하고 있어요. 그 중에서도 로케이션 코어팀은 Go 언어로 서버를 개발하고 있는데요.저는 당근에 입사하기 이전에 Go를 사용해본 경험이 없었어요. 로케이션 코어팀에 합류하고 업무를 진행하면서 Go를 온몸으로 마주하게 되었죠.오늘은 그 과정에서 제가 마주했던 고민들을 공유해보려 해요. 더불어 로케이션 코어팀이 업무 효율성을 높이기 위해 Go를 어떠한 방식으로 사용하고 있는지, 그 방식이 저에게 어떻게 긍정적으로 작용했는지까지 함께 소개해볼게요! Golang이 궁금하셨던, 그리고 당근에서 Golang을 업무에 사용하는 방식이 궁금하셨던 분들에게 도움이 되는 글이기를 바라요 신입 Go-pher가 바라보는 Go 언어는..공식 홈페이지에서 Go를 소개하는 문장이에요.Build simple, secure, scalable systems with Go (Golang Official Catchphrase)Go는 다른 프로그래밍 언어에 비해 키워드 수가 적고 문법이 단순하여 배우기 쉽고, (simple) 컴파일러와 강타입(strongly-typed) 시스템을 기반으로 프로그램의 안전성 (secure)을 높일 수 있어요. 고루틴을 통해 동시성을 손쉽게 관리할 수 있어 대규모 서버와 분산 시스템에서 사용하기에도 유리해요. (scalable)이처럼 사용하기가 쉽고 자율성이 높다는 장점이 있지만 그만큼 낯설고 모호한 부분도 존재했어요.클래스가 없는 객체 지향 언어?Go언어는 보편적인 객체 지향 언어와 다른 성격을 띄고 있어요.놀랍게도, 클래스가 존재하지 않아요. 구조체(struct)를 사용하여 비슷한 역할을 만들어낼 수는 있지만 클래스처럼 메서드를 내장하지는 않아요. 대신 메서드를 정의한 인터페이스(interface)를 생성하고, 메서드가 해당 타입의 인스턴스에 속해 있는 것처럼 구현하여 다형성을 적용해요. 이때 func 키워드와 함수명 사이에 구조체를 선언하여 구조체와 메서드를 연결할 수 있는데, 이를 리시버(receiver)라고 불러요.type SampleInterface interface { SampleMethod()}type SampleStruct struct { Name string Data map[string]string}// 리시버로 구조체와 인터페이스의 메서드를 연결하여 다형성을 적용func (s SampleStruct) SampleMethod() { fmt.Println("implement sample method")}func (s SampleStruct) GetName() string { return s.Name}func main() { s := SampleStruct{Name: "name", Data: make(map[string]string)} var i SampleInterface i = s i.SampleMethod() // 인터페이스에 정의된 메서드 호출 fmt.Println(i.GetName()) // ERROR}개발 방법론의 유연함Go언어는 표준화된 개발 방법론이 서비스 개발에 강제되지 않아요. Go 자체의 컨벤션이나 여러 가이드들은 존재하지만, 사람마다 코드 작성 방식이 다를 수 있어요.특히 제가 입사를 했을 때인 약 1년 전까지만 해도 로케이션 코어팀 대부분의 팀원이 합류한지 3개월이 채 되지 않은 상태였고 운영 중인 프로젝트들도 각기 다른 맥락과 구조를 가지고 있었어요. 따라서 표준화된 구조를 가져가기보다는 각자 관리 중인 프로젝트의 특성에 맞게 유연한 개발을 진행하는 것이 더 효율적이었죠.프로젝트마다 geometry를 처리하기 위해 사용하는 라이브러리가 전부 달라요!하지만 그러다 보니 동일한 기능을 수행하는 코드 베이스임에도 불구하고 레포마다 각기 다른 라이브러리를 기반으로 구현되어 있어 히스토리를 파악하기 어렵거나 특정 프로젝트에서만 알 수 없는 버그가 발생하는 등의 문제도 있었어요.특정 프로젝트가 사용 중이던 라이브러리가 아카이브 되어버린 경우도 있었어요로직에 집중해야 하는데, 고민해야 할 것이 너무 많다?입사 당시 저는 모노레포로 관리 중인 저장소에 새로운 서버를 구성하는 과제를 2달 간 진행했는데요. 테크 스펙을 작성할 때 위에서 언급한 Go의 여러 특성들 때문에 아래와 같이 방법론적으로 고민해야 할 부분이 많았어요.그러다보니 로직 구성과 같은 정말 중요한 부분을 놓치고 있는 것 같다는 생각이 들기 시작했어요.우리 팀에서는 이런 방법을 사용했어요공유 가능한 Go 라이브러리 구현이러한 고민을 저만 하고 있던 것이 아니었던 것 같아요.왜냐하면 비슷한 맥락에서 코드의 일관성을 적정 레벨로 유지하며, 반복되는 작업을 최소화하여 생산성을 향상시키기 위해 팀 내에서 (일부 프로젝트에서만) 부분적으로 적용하고 있던 방식이 있었거든요.바로 공유 가능한 Go 라이브러리를 구현하고, 각 어플리케이션에서 필요에 따라 패키지나 모듈을 독립적으로 호출하여 사용하는 방식이에요.Fx라는 Go의 의존성 주입 라이브러리를 감싸 공유가 가능한 최소 단위의 모듈을 구현했어요. 구현된 모듈은 Database, Message Queue, Redis, Logger, gRPC Server 등 어플리케이션에서 필요한 여러 컴포넌트들을 제공해요.Fx는 Uber에서 개발한 Go 언어용 의존성 주입 라이브러리로 어플리케이션의 구성 요소들 사이의 의존성을 관리하고, hook에 정의한 lifecycle에 따라 객체를 생성 및 연결해주는 기능을 제공해줘요.아래와 같이 모듈을 생성할 수 있어요.독립적으로 호출할 fx.Module 변수를 정의해요.var Module = fx.Module("pg",2. fx.Provide 구문으로 module에 필요한 생성자들을 추가해요.var Module = fx.Module("server", fx.Provide(parseCon
go
(당근!) 반가운 중고거래 키워드 알림 만들기
안녕하세요. 중고거래실에서 iOS 엔지니어로 일하고 있는 Lychee예요.당근 중고거래, 다양한 물건을 사고팔 때 자주 이용하실 텐데요. 찾고 있는 물품이 올라왔을 때 바로 알림을 받을 수 있는 키워드 알림 기능을 아시나요?작년 가을 쯤 노트북이 필요해서 키워드 알림으로 노트북 기종을 등록해두었던 적이 있어요. 제가 찾던 기종의 글이 올라올 때 바로 알림을 받을 수 있어서 좋았죠. 그런데 종종 노트북 케이스, 노트북 충전기와 같이 등록한 키워드가 포함된 다른 물건의 알림을 받기도 했어요. 이럴 때는 기다리던 알림이 아니라서 실망하기도 했었어요.그 즈음 저희 팀은 중고거래 리텐션에 중요한 역할을 하는 키워드 알림을 자세히 들여다보기 시작했어요. 키워드 알림을 등록하는 지표는 꾸준히 성장하고 있으나, 발송된 키워드 알림 푸시의 양에 비해 낮은 오픈율을 보았을 때 사용자들이 원하는 알림을 받지 못하고 있다는 걸 알 수 있었죠. 그렇게 원치 않는 알림으로 피로가 쌓이는 것을 방지하기 위해 이 문제를 해결하기로 했어요.Before > 기존 키워드 알림 등록 화면키워드 알림의 문제를 더 자세히 들여다보기 위해 사용자의 목소리를 들어보았어요. ️ 제가 찾는 물건이 아니에요. 등록한 키워드와 정확하게 일치하는 알림만 받고 싶어요 ️ 제가 찾는 물건이라도, 너무 먼 동네의 알림은 받고 싶지 않아요 ️ 내가 원하는 물품의 키워드 알림을 받으려면 어떤 키워드를 등록해야할지 모르겠어요 다양한 사례를 생생하게 들으며, 키워드 알림의 문제를 더 뾰족하게 정의했어요. 그리고 문제에 대한 개선점을 발굴해나갔고 그렇게 키워드 알림 개선 프로젝트를 시작했어요. 클라이언트 릴리즈 로그를 돌아보니 지난 5개월 동안 아주 긴밀하고 치열하게 몰입할 수 있는 시간을 보낸 것 같아요 5개월 간의 클라이언트 Release Log개선 프로젝트는 크고 작은 여러 개의 실험과 기능 개선을 반복하는 사이클로 진행되었어요. 키워드 알림 등록 및 관리에 대한 전반적인 기능 개편과 함께 더 적절한 키워드를 등록하도록 돕는 여러 추천 실험 등을 반복했죠. 그 결과 키워드 알림 오픈율이 2배 가까이 상승하며, 키워드 알림에서 구매를 위한 채팅으로 전환하는 수도 함께 증가하는 것을 볼 수 있었어요. 이렇게 제품의 성장을 위해 빠르게 개선을 반복하는 과정에서, 저에게 큰 도움이 되었던 유연한 모바일 설계에 대한 이야기를 나눠보려고 해요!독립적인 환경, Feature Module 기반으로키워드 알림 Feature Module당근 iOS 앱은 Feature, UI, Domain, Core, Shared의 5가지 계층을 가진 모듈로 구성되어있는데요. 새 기능을 추가할 때마다 Feature Module을 추가하지는 않지만, 분리될 수 있는 맥락의 기능이라면 별도의 Feature Module을 생성하는 것을 권장하고 있어요. (당근 iOS챕터의 모듈화에 대한 자세한 내용은 이 글을 참고해주셔도 좋아요.) 새 술은 새 부대에 라는 격언에 따라 새로 개선되는 키워드 알림 기능을 담을 별도의 Feature Module을 생성했어요. 그리고 새 모듈 위에 키워드 알림에서 사용할 소스코드들을 설계해 쌓아올렸어요.FleaMarketKeywordNotificationFeature 모듈은 다른 Feature 모듈과 앱 타겟과는 독립적으로 분리가 되어있어서, 새 기능을 끼워넣을 때 발생할 수 있는 사이드 이펙트로부터 비교적 안전한 환경이었어요. 덕분에 적은 부담감으로 빠르게 작업을 해나갈 수가 있었지요. 또 전체 모듈을 빌드하지 않고도 단독 모듈에서 빌드와 테스트를 수행할 수 있기에 이전보다 시간을 단축할 수 있어서 생산성도 끌어올릴 수 있었어요.초기부터 변화에 유연하게 대응할 수 있도록 구성키워드 알림 개선의 첫 과제로, 가장 많은 불편사항으로 꼽혔던 불필요한 키워드 알림 을 줄이기 위한 키워드의 조건을 설정할 수 있는 기능을 추가하게 되었어요. 기존에는 키워드만을 등록할 수 있었는데요, 이제 등록한 키워드의 카테고리, 가격 범위, 제외하고 싶은 키워드와 같은 조건을 설정해서 필요한 알림만 필터링해서 받을 수 있도록 한 것이죠.After > 키워드의 조건을 설정할 수 있는 기능이 추가된 새 키워드 알림 설정화면새 키워드 알림 설정화면 UI의 초기 스펙은 단일 세로형 목록으로 구성되었어요. (왼쪽 이미지 참고) 하지만 키워드 알림에 대한 여러 불편함을 해결하기 위해서는 리스트의 구조가 여러 가지 형태로 변화될 가능성을 열어두어야 했죠. 그래서 처음엔 비록 Verticle List의 단순한 형태이지만 enum으로 Section 타입을 정의했고, 이에 따라 리스트를 구성하도록 했어요.리스트 UI는 KarrotListKit을 사용하여 구성했는데요. List, Section, Cell을 직관적으로 구성하기 때문에 컴포넌트 단위로 관리하기가 용이했어요. 또 각 컴포넌트에 modifier를 붙여 간단하게 레이아웃 속성을 지정하거나 컴포넌트에 발생하는 액션을 받도록 연결할 수 있어서 UI 구성 단계의 생산성을 올려주었어요.enum Section: Equatable, Identifiable { case registeredUserKeywordList([ItemViewModel])}func updateSection(section: Section) { collectionViewAdapter.apply( List { sections.map { section -> Section in switch section { case .registeredUserKeywordList(let viewModels): return Section( id: id, cells: makeRegisteredKeywordSectionItems( viewModels: viewModels ) ) .withSectionLayout(.vertical(spacing: 0.0)) } } } )}리뉴얼된 세로형 리스트의 키워드 알림 설정화면을 배포 후, 다음 작업으로 키워드 등록을 제안하는 실험을 하기로 했어요. 키워드 알림 설정 화면에 방문했을 때 어떤 키워드를 등록해야할 지 어려움을 겪고 있을 것이라는 가설이었죠.