본문 바로가기
1️⃣ 개발 지식 A+

디자인 패턴 실무 적용기

by ddubbu 2026. 3. 8.

디자인 패턴을 처음 알게된 것은 2022년도 어느날, iOS 팀장님 세션이었다. 몇주간 진행된 세션이 종료되고 나는 유레카를 외쳤다. 한창 프론트 개발자로서 마크업과 비동기 처리 작업이 전부인줄 알았었고, 아키텍처, OOP, 디자인 패턴은 백엔드의 전유물로만 알았던 내게 시야가 넓어지는 순간이었기 때문이다. 그래서 신규 작업이 들어오면 어느 곳을 추상화할지 고민하고 'FE clean architecture best practice' 를 검색하곤 했다. 하지만 여전히 갈증이 해소되지 않아 https://github.com/ddubbu-dev/Study-Design-Patterns 레포지토리에 다양한 강의들을 섭렵하며 개념을 넘어 '코드'로 이해하고 싶었다. 그리고 2026년 현재, 조금은 추상화&적합한 디자인 패턴 적용이 자연스러워진 것 같아 그 경험을 공유해보려한다.

 

요청사항1
상품 카드를 개선해야합니다. 반짝 할인 기능이 1차 배포, 쿠폰 기능이 2차 배포로 진행될거에요.

(좌) 반짝 할인 기능 (우) 쿠폰 기능

 

추상화에서 항상 고민하는 부분이 OCP 즉, 어디까지 기능 확장을 허용할지이다. 보통은 요구사항에 한정해 사고하기 때문에 이후 요구사항이 추가되면 SRP를 위반하고 다수의 기존 파일을 건드려 diff가 커지곤했다. 하지만 이번 요구사항은 거의 답을 떠먹여주었다. '1차 작업물에서 2차 작업물로 기능 확장될거야~' 라고 알려주었고 세부 로직은 다를지언정 큰 Layout은 똑같았기 때문이다.

 

먼저 UI 추상화를 진행했다.

- Discount Price: 기존가에서 할인이 적용될 경우 초록색으로 하이라이트되고, 기존가를 그 아래 표시한다.

- Discount Tile: 할인 유형 (특정 시간 동안만 or 유저 클릭 기반)

 

Discount Price

1. '상품카드-할인'이라는 도메인 하에 비즈니스 로직만 다를 뿐이라, 할인 유형별로 비즈니스 로직을 갖는 ViewModel 을 정의하고 현재 진행중인 할인 이벤트 (RunningDiscountEvent) ViewModel은 이들을 구독할뿐 (참고로 Svelte를 쓰고 있어 derived store 기능을 사용한다) 할인 유형은 몰라도 된다고 생각했다.*

*이후 생각이 바뀜

// 초기 설계
type RunningDiscountEvent {
  discountDisplayPrice: string;
  discountPricePostfix: string;
}

 

 

2. 여기서 잠깐, MVVM Pattern을 FE 코드에 어떻게 도입했는지 알아보자. 해당 개념을 배우기 전에는 data source 와 user state를 분리하지 않아 자주 바뀌는 UI 요청사항에 data model 수정이 빈번했고 간단한 수정사항에 비해 diff는 커졌다.

 

일례로 '유저가 클릭한 timestamp에 blue dot을 표시한다'가 초기 디자인이었다. data model은 'hasBlueDot' 필드를 가졌고, view는 리스트 데이터를 순회하면서 이를 직접적으로 소비했다. 하지만 리뷰를 받으면서 깨달은 몇가지

- blue dot이 리스트 중 한 아이템에만 있거나(어디에도 없다면), 다수 아이템은 불필요하게 false 필드를 관리해야한다. 특히 blue dot 상태를 업데이트하기 위해 전체 리스트를 매번 수행해야했음

- 또한 blue dot 이 아니라 다른 UI로 바뀌게 된다면, data source 에는 변화가 없는데 model 이 view와 직접적으로 결합되어 있어 model 수정이 불가피한 구조였다.

 

그렇게 몇 주 지나지 않아 새로운 디자인이 들어왔고, blue dot이 사라지고 말았다. 아뿔싸 Σ(; ・`д・´)

(좌) 초기 디자인 (우) 변경된 디자인

 

그래서 view-model 필요성을 뼈저리게 느끼고 현재는 다음과 같이 최소한 layer를 분리하려고 노력하고 있다.

- model: API 등 외부로부터 들어오는 데이터, CRUD action

- view-model: UI 및 user state

- view: view-model만 소비, model 을 직접적으로 의존하지 않도록 주의!

 

3. 그 덕에 비즈니스 로직에 집중할 수 있었고, 다음과 같은 이점이 있었다.

- 할인 유형에 따라 (discountType=percent, amount) 계산 로직을 적용하고, SRP가 보장되었다.

- 혹시 할인 유형, 계산로직이 변경되더라도 diff는 적고 쉽게 적용할 수 있어, OCP도 보장되었다.

 

Discount Tile

4. 문제는 Tile 이었다. Price는 원가, 할인가 데이터 기반으로 UI 가 동일했지만, Tile은 도저히 추상화를 할 수 없었다.

- 타이머 기능 (Time Driven), 이벤트 기간 만료되면 Tile 사라짐

- 쿠폰 적용하기 (User Interaction Driven), 할인 적용시 Tile 사라짐

 

그래서 Compound Component Pattern 을 적용했다.

- "Tile 이 등장하고 사라지는 Animation 기능"만 관리해주는 상위 Container를 정의

- 비즈니스 로직은 각 컴포넌트, ViewModel에서 관리하기로 했다.

 let cachedTile: TileCache | null = null; 
 
 // 생략
  {#if cachedTile}
    {#if cachedTile.type === "flash-deal"}
      <FlashDealTileComponent tileInfo={cachedTile.info} />
    {:else if cachedTile.type === "coupon"}
      <CouponTileComponent tileInfo={cachedTile.info} />
    {/if}
  {/if}

 

5. 이때도 다양한 좌충우돌이 있었다. 사실 맨 처음에는 캐싱 기능이 없었다. Tile 데이터를 제거하면, Animation이 시작되는데 duration으로 인해 Tile 컴포넌트는 즉시 사라졌고, 비어보이는 순간이 있던 것이다. 그래서 Animation 종료되기 전까지는 Tile 데이터를 갖고 있어야하는데, 이를 View Model이 관리하기는 적합하지 않다고 생각했다. View Model 이 View Animation 종료 상태를 알아야는 의존도가 생기며 기이한 구조가 되었기 때문이다. 그래서 "캐싱"으로 문제를 해결했다.

 

(View Model -> 데이터 제거 -> View 애니메이션 시작) 심플한 구조

(View Model -> 데이터 제거될거야, 애니메이션 종료되면 그때 완전히 제거돼  -> View ?? 애니메이션 언제 시작하라고??)

 

 

6. 이제 상속은 식은죽 먹기

타이머 기능 또한 시간이 null일 경우 표시되지 않고, 값이 존재할때만 표시되었다. 그래서 기본적인 할인 이벤트 기능을 정의한 베이스 class FlashDealThisEvent 를 기반으로 타이머 기능을 추가한 class FlashDealTimer 를 확장 정의하였다.

class FlashDealTimer extends FlashDealThisEvent

 

7. 위 구조는 다음과 같은 이점이 있었다.

- 할인이 여러개 적용중일 때 첫번째 할인이 만료되어도 두번째 할인이 등장하며 공존할 수 있었다. 할인 유형별로 Model 데이터를 저장하고 있고 ViewModel 내에서 if문 순서만 바꾸면 되었기 때문이다.

- 타임 기반 이벤트의 경우 상품 카드 뿐만 아니라 네비게이션 뱃지 UI에도 영향을 주었는데, 비즈니스 로직이 뭐던 간에 ViewModel에서 편히 가공하면되므로 신규 요청사항에 두려움이 없었다.

 


 

요청사항2
새로운 로깅툴(B)을 도입할거에요. 근데 몇가지 조건이 있습니다.
- 기존 로깅툴(A)에서 로깅하는 일부 이벤트만 수집합니다.
- 주기성 이벤트는 A에서 1초마다 수집되었다면, B에서는 5초마다 수집할거에요.

 

두번째 작업은 정말이지 혼란스러웠다. (B)는 OpenTelemetry라는 도구였는데, 이를 채 익히기도 전에 Client 개발자 각자가 작업하다보니 이해도에 따라 정말 다르게 수집되었기 때문이다. 다행히도 내가 맡았던 웹 배포일은 아직 미정인 상황이라 테스트용으로만 빠르게 코드를 작성했고, 한 파일에 모든 기능을 정의하였다. 코드 중복은 물론이고, otel-sdk 는 사실 이벤트 도메인을 알 필요가 없는데 특정 이벤트를 필터링하고 로직을 추가하기 위해서 어쩔 수 없이 의존도가 생겨버린 것이다.

 

 

 

1차 고민: 1차 시연 이후 시간 여유가 생기자, 당장에 리팩토링 방향성을 고심했다. 

- 초기 의존도: (B) <-> (A)

- 우선 찝찝함을 갖고 있던 이벤트 도메인에 대한 의존도를 끊어내었다. otel-sdk 는 "데이터를 전송"에만 집중했다. (SRP)

- 집단지성으로 OpenTelemetry 이해도가 높아졌다. 초기 요구사항에는 metric 타입만 수집하기로 했으나 log 타입이 충분히 추가될 수 있다고 생각되어 타입별로 method를 정의했다. (OCP)

- 데이터 수집 타입과 상관 없이, 공통의 데이터 모델에 기반해 각기 요구사항에 맞게 body 를 구성하면 되었음

- 최종: 추후 새로운 이벤트를 적재하여도 otel-sdk를 호출하기만 하면됨!

 

2차 고민: 근데 (A), (B) 의존도를 어떻게 끊어내고 어떻게 특정 이벤트만 전송할 수 있을까?? adapter 패턴 적용

- 개선된 의존도: (B) -> PlaybackOtelAdapter <- (A) (DIP)

- (A), (B) 이 둘을 아는건 adapter class 뿐이다! body 구성도, 필터링도 모두 얘가 담당.

 

3차 고민: otel-sdk class 가 너무 여러번 초기화 되는데?? signleton 패턴

- 처음에는 singleton 으로 구성하지 않았다. 하지만, video player 특성상 리로드를 위해 DOM 을 부시는 과정이 있어 내부 로직도 함께 초기화 되는 것이다 (new 키워드 사용했음).

- 그래서 최종은 singleton 패턴으로 초기화는 앱이 처음 실행될 때만 발생하고, 이로 인해 내부 멤버 변수로 정의한 metric service name은 결국 parameter로 받아 순수 otel-sdk 가 완성되었다.

 

이를 작성한 순간에도 request body 요청사항이 바뀌었지만 괜찮다. 특정 함수만 바꾸면 된다는 것을 알기 때문이다. 두렵지 않다.

 


 

지금 돌이켜보면 여전히 부족한 점이 많고, 그 당시에도 제한된 시간 내에서 아쉬움이 남은 채 머지하기도 했다. 또한 과도한 추상화로 동료가 디버깅에 손들어 버린 부끄러운 순간도 있었다. 하지만 "내 추구미는 최소 DIFF"로 잘 배운 OCP를 지켜보고자 계속 노력하고 있어 뿌듯하다.

동료와의 쓰레드; 내 추구미는 최소 DIFF

 

글을 쓰다보니 스타트업씬의 잦은 요구사항 변화에 "두렵지 않다" 는 말을 자주 언급했음을 깨달았다. 한편으로는 최근 다양한 AI 도구가 생겨나면서 문득 지식을 얻는 공부는 시대에 뒤처지나? 라는 생각이 들기도 했다. 하지만, 탄탄한 지식을 토대로 고퀄리티 PR 리뷰를 하고, AI가 뱉는 답변에 비판적인 사고를 할 수 있는 힘은 여전히 '지식'이라고 확신하며 마음을 다잡았다.

 

AI는 도구일뿐 더더욱 기본기를 다져야겠다고 생각했다. (AI에 대한 회고는 후속 포스팅에서 다루겠다)

긴글 읽어주셔서 감사합니다.