안녕하세요, 피에프씨테크놀로지스(PFCT) Client Team iOS 롤리더 김규리입니다.
얼마 전 이탈리아 토리노에서 열린 iOS 기술 컨퍼런스 SwiftHeroes 2024 후기에서 인사드렸었는데요!

(아직 안보셨다면? 우리의 기술력을 세계에 펼치다, ‘SwiftHeroes 2024’ 컨퍼런스 후기 보러가기)

컨퍼런스 현장 사진

저희는 컨퍼런스 첫째 날, 컨퍼런스에 참석한 세계 각국의 iOS 개발자들 앞에서 <Your Big Head Component Spoils Your Code> 라는 주제로 발표를 했어요. 어떻게 준비했고, 어떤 내용을 담았는지 지금부터 이야기 해보려 합니다!

우선, 우리 팀이 자신있는 주제로 발표하고 싶었어요. 그래서 고른 게 바로 ‘SwiftUI’ 였죠.

PFCT가 처음 앱에 대출 프로세스를 구축하던 3년 전 만해도 UIKit을 사용하는 게 보통이었는데요. 저희는 과감하게 신기술이었던 SwiftUI 를 도입했습니다. 스타트업이라 빠른 실험이 중요했거든요. SwiftUI 는 보일러플레이트 없이도 가독성 높은 코드를 빠르게 개발할 수 있어서 매력적이었습니다. 물론 신기술을 쓴다는 개발자로서의 도전 정신도 빼놓을 수 없었고요!

이렇게 초기 SwiftUI 부터 경험하며 그동안 해결해왔던 관련 기술적 문제는 여러가지가 있었지만, 그 중에서도 선언형 사고방식에 대해 짚고 싶었습니다.

iOS 개발자로서 가장 먼저 접근하게 되는 UI Layer 를 다뤄보자는 생각이 들었거든요. 선언형 프레임워크 컴포넌트 설계 라는 키워드를 잡고 나니 제안서가 술술 써졌던 기억이 납니다!

네이티브 앱 개발을 하면 가장 먼저 경험하는 것이 UI 죠. UI 중에서도 단순 화면 UI 개발부터 네비게이션 등의 시스템 개발까지, 다양한 스펙트럼 중 네이티브 개발자들이 자주 경험하는 것 중 하나가 컴포넌트 개발입니다.

컴포넌트는 UI Layer 에서 의존성이 꽤 높은 영역이지만 시스템 설계보다는 의존성이 낮다 보니 힘을 빼는 경향이 있어요. 그렇지만, UIKit 의 사고방식을 고수하다 보면 SwiftUI 가 제공하는 선언형 프레임워크만의 매력적인 장점을 놓칠 수 있습니다.

특히 PFCT와 같은 금융권은 다양한 관심사의 데이터 묶음을 요약된 정보로 보여주어야 하는 경우가 많은데요. 복잡한 컴포넌트를 다룰수록 유지보수를 위해 주요 컴포넌트 설계는 필수적입니다.

청중들이 우리가 겪은 문제에 깊게 공감해줬으면 하는 마음에 스토리텔링으로 발표를 구성했어요. 실제로 경험했던 PO(Product Owner) 와 개발자 간의 대화를 각색해서 발표를 구성했는데, 그 시작을 열었던 문장입니다.

PFCT의 대출/투자/신용관리 등 모든 서비스를 만나볼 수 있는 ‘크플’ 앱에는 유저의 신용 상태를 집약적으로 보여주기 위해 다수의 그래프가 사용됩니다. 그러나 초기 ‘크플’의 최소 버전은 iOS 15로, Apple 에서 제공하는 SwiftCharts 를 사용할 수 없었어요. 대표적으로 도넛 모양의 원형 그래프가 자주 활용되는데, 맨 처음 디자인 시안은 다음과 같았습니다. (모든 시안은 효과적인 발표를 위해 각색 되었어요.)

첫 번째 시안 – 차트의 섹션별로 주요 색상만 다름

개발자가 가장 쉽게 떠올릴 수 있는 인터페이스는 아래와 같아요. 이 방식은 모든 변경 가능한 값을 파라미터로 주입 받습니다.

//
//  DonutChart.swift
//

import SwiftUI

struct DonutChart: View {
    var items: [Item]

    var body: some View {
        // some implementation
        EmptyView()
    }
}

extension DonutChart {
    struct Item {
        var id: Int
        var percentage: Int
        var color: Color
    }
}

그런데 비슷한 UI 의 도넛 그래프가 다음과 같이 재활용되어야 한다면 어떨까요?

여기서 중요한 점은 각각의 시안이 하나의 화면에서 바뀌는 것이 아니라, 다른 화면에서 하나의 컴포넌트를 재활용하여 각각 구현되어야 한다는 것입니다.

(왼) 두 번째 시안 – 유저가 섹션을 선택할 수 있고 각 섹션의 라벨을 보여줘야 함
(오) 세 번째 시안 – 각 라벨이 다양화 되고, 여러 섹션을 선택할 수 있으며 그림자 등 다양한 UI 요소가 추가됨

이전과 동일하게 파라미터로 추가하면 다음과 같은 인터페이스가 발생할 수 있습니다.

import SwiftUI

struct DonutChart: View {
    @Binding var selectedItems: [Item]?
    var items: [Item]
    var scaleEffectValue: CGFloat
    var stroke: StrokeStyle
    var labelFont: Font
    var labelForegroundColor: Color
    var labelBackgroundColor: Color
    var selectedLabelFont: Font
    var selectedLabelForegroundColor: Color
    var selectedLabelBackgroundColor: Color
    var shadowEffect: ShadowStyle
    var animation: Animation

    var body: some View {
        // some implementation
        EmptyView()
    }
}

extension DonutChart {
    struct Item {
        var id: Int
        var percentage: Int
        var color: Color
        var label: String
    }
}

바로 이 상황이 발표의 주요 골자로 등장하는 Big head component 의 정의입니다. Big head component의 문제가 뭘까요?🤔

iOS 개발자로서 가장 공감할만한 문제는 인스턴스의 초기화 시점에 발생합니다.

동일한 선언형 프레임워크인 React 에 비해 Swift 가 갖고 있는 단점은 파라미터의 순서와 값을 명시해야 한다는 것입니다. 파라미터가 많아질수록 초기화의 피로도가 상당히 높아지며, default 값이 없는 한 개발자는 해당 컴포넌트를 사용할 때마다 n개의 파라미터의 순서를 지켜 적절한 값을 주입해야 합니다. 그렇지 않으면 아래와 같은 에러를 만나게 돼요.

Swift 를 사용하면 자주 만나게 되는 에러 메세지

기술적으로는 4가지 문제점이 존재합니다.

1) Out of Control

  • 다수의 개발자가 유지 보수하기 어려우며 과도한 분기 로직으로 인해 로직 복잡성이 증가합니다.

2) Uncertain Testability

  • 예측 범주를 벗어나는 side-effect 에 대한 모든 경우의 수를 테스트하기 어려워집니다.

3) Useless Dependency

  • 컴포넌트 재활용 시에 해당 유즈케이스에서 필요하지 않은 파라미터까지 주입해야하는 의존성 문제가 발생합니다.

4) Resource Inefficiency

  • 제한된 리소스로 컴포넌트를 관리하기 어려워집니다.
과도한 파라미터를 생산하게 되면 마주하는 기술적 문제점 4가지

이 문제를 해결하기 위해서는 파라미터의 개수를 줄여야 합니다. 그런데 어떻게 파라미터 없이 객체에 데이터를 전달할 수 있을까요? 그 해답은 선언형 사고방식(Declarative Thinking)에서 찾을 수 있습니다.

SwiftUI 의 가장 단순한 예인 Image를 떠올려봅시다.

Image(systemName: "bell.fill")
  .renderingMode(.template)
  .resizable()
  .foregroundStyle(Color.red)
  .frame(width: 20, height: 20)

이곳에서 Image 는 본질에 해당하고, 그 외의 ViewModifer들은 모두 메소드 체이닝 (method chaining) 을 통해 Image 를 꾸며줍니다. 이미지의 색상과 크기를 바꾸었지만 파라미터는 이미지 이름 뿐입니다. 디자인 패턴 중 데코레이터 패턴(Decorator Pattern)을 이용한 방식이죠.

컴포넌트의 재활용성을 높이기 위해서는 초기 모델 설계를 잘 해두거나 Generic View, Template pattern 등의 방법을 활용하는 것이 있습니다. 해당 발표에서는 이 예시에 활용된 SwiftUI 의 ViewModifier 에 초점을 맞추어 앞선 문제를 해결했습니다.

복잡한 컴포넌트일수록 선언적 사고방식을 더 잘 사용해야 함에도 불구하고 유지 보수가 어려운 방식으로 구현하는 실수가 많은데요. 원래 확장성 높은 코드를 구현하기 위해서는 몇 가지 개념과 약간의 까다로운 기법이 필요하지만, 대게 ViewModifer 를 구현하는 가장 쉬운 방법을 택하기 때문입니다.

SwiftUI의 View 타입에 extension 으로 구현하는 것이죠. 그렇지만 이렇게 된다면 return type 이 some View 일 수 밖에 없어 몇 가지 문제가 발생합니다.

import SwiftUI

extension View {
    func resiazble() -> some View  {
	// 구현 생략
    }
}
  1. Namespace Pollution | 과도하게 많은 옵션이 어디에서나 접근 가능하도록 열려있습니다.
  2. Overriding Conflict | 기존에 정의된 함수와 overriding 충돌이 발생할 수 있습니다.
  3. Encapsulation and Modularity | 재활용성이 떨어져서 모듈화가 어렵습니다.
  4. Testing and Maintenance | 파라미터 수가 늘수록 사용 가능한 경우의 수도 함께 증가하여 테스트 복잡도가 높아집니다.
Extension View 에서 ViewModifer 들을 구현할 때의 문제점

쉬운 예시를 들자면, Text를 구현하고 싶은데 Image 에 해당하는 ViewModifier를 만나게 되는 것이죠. 개수가 많아질수록 사용하는 데 혼란스러움은 증가할 것입니다.

Text 하위에서 Image를 위한 ViewModifer 를 마주하는 상황

이 문제를 해결하기 위해서는 몇 가지 trick이 필요합니다. 기존의 SwiftCharts 에서 착안하였는데요. 발표에서 이 과정을 단순화한 공식을 만들어 소개했습니다.

선언적 사고방식으로 컴포넌트를 설계하기 위한 공식

Step1. Implement Basic Component

  • 컴포넌트의 본질적 의미를 이해하고, 필수 파라미터를 정의하여 컴포넌트를 구현합니다.

Step2. Define Type-erased Protocol

  • 통신을 위한 type-erased 프로토콜 타입을 정의합니다.

Step3. Make wrapper to Wrap Any View

  • 어떠한 View 도 Step2에서 정의한 프로토콜을 따를 수 있도록 View 를 wrapping 하는 Wrapper 를 구현합니다.

Step4. Create ViewModifers

  • 정의한 Type-erased 프로토콜에 n개의 ViewModifier를 마음껏 구현합니다. 여기서 확장성을 보장하게 되며 분기로 인한 각각의 로직을 분리할 수 있게 됩니다.

Step5. Utilize Environment and Inject Them

  • ViewModifer 로 해결되지 않은 데이터는 Swift의 Environment 을 활용하여 뷰에 주입합니다.

Step6. Use it!

가장 먼저 ISP (Interface Sergregation Principle)를 준수하기 위한 Any 타입이 필요합니다. 우리는 도넛 차트를 구현하고자 하기에 AnyDonutChart protocol을 정의했어요. 프로토콜 통신을 통해 강결합을 끊고 확장성을 보장하기 위해서에요. View 타입이 아닌 AnyDonutChart 의 타입으로 메소드 체이닝이 가능하도록 구현하는 것이 우리의 목표입니다.

import Foundation
import SwiftUI

protocol AnyDonutChart: View {}

(약한 결합의 통신을 위한 AnyDonutChart 프로토콜)

이제 AnyDonutChart 에 ViewModifer 를 마음껏 구현해볼까요?

AnyDonutChart 에 ViewModifer 를 구현하는 코드. 아직 return 타입이 some View 이기 때문에 문제가 있다.

이 과정에서 하게 될 고민은 어떻게 return type 을 some View 가 아닌 type-erased 프로토콜로 변화시킬 수 있는지 입니다. 이는 Wrapper 를 통해 생각보다 쉽게 해결할 수 있습니다.

type-erased 프로토콜로 Method Chaining 을 활용하기 위해 구현한 Wrapper (ISP 준수)

그런데 아직 해결되지 않은 고민이 있죠. 바로 데이터 전달입니다. 이 과정에서는 실무 개발자들간의 약속이 필요합니다. 과도하지 않은 선에서 파라미터로 추가할 주요 데이터의 기준을 정할 수도 있죠. iOS 팀은 파라미터를 지양하고 Environment 를 통해 확장하는 방식을 채택했습니다.

Wrapper 를 통해 some AnyDonutChart 로 리턴하는 예시 코드

사실 Environment 를 남용하는 방식은 좋지 않습니다. 약간의 보일러플레이트를 해결해야 하는 이슈도 있고, 무엇보다 접근성의 관점에서 기존 우리가 목표했던 ISP 준수에 어긋날 수 있기 때문이에요. 그러나 View 에 Extension을 허용하는 것과는 달리, 주입하고자 하는 뷰에 Environment 를 선언하지 않으면 접근성이 제한된다는 점과 EnvironmentKey 값을 통해 명확히 구분할 수 있다는 장점을 취하기로 결정했습니다.

공식을 통해 다시 도넛 차트를 구현하게 되면 인터페이스가 다음과 같이 변화될 수 있습니다.

(왼) 기존의 Big Head 컴포넌트 / (오) 선언형 방식으로 구현한 컴포넌트 사용 예시

이 선언형 사고방식으로 코드의 유지보수의 난이도를 낮추고 확장성을 크게 높였습니다. 실제로 ‘크플’은 현재 이 방식으로 구현된 컴포넌트를 사용하고 있으니 한 번 구경해보시는 것도 좋을 것 같아요! ( ’크플-요즘 돈 버는 법’ 어플 다운로드 바로가기)

발표가 끝난 후 연락을 많이 받았어요. 실시간 온라인 티켓을 구매해서 들은 청중들도 다양한 플랫폼을 통해 강연 잘 봤다는 말을 건넸고, 추가적으로 궁금한 것들을 물어보기도 했어요. “UIKit 개발자이지만 SwiftUI로의 전환을 고려하고 있고, 당신의 Insight 가 도움이 되었다”는 이야기도 있었죠.

기술 컨퍼런스에 참석하는 것도 처음이었지만, 첫 컨퍼런스를 연사로서 참석하게 될 줄도, 그 첫 발표를 이탈리아에서 영어 발표로 진행하게 될 거라고는 생각하지 못했기에 더 특별했습니다.

발표 자료의 소개 장표에 ‘PFCT’ 로고를 넣을 때 많은 생각이 들더라고요. 곧 PFCT에 합류한 지 4년 차가 되는데, 그동안 쌓아온 프로젝트와 코드의 결과물이 주마등처럼 스쳐 지나가는 듯 했어요.

앞으로 더 좋은 금융 앱을 만들고자 노력할 PFCT iOS팀과 ‘크플’ 앱에 많은 응원과 관심 부탁 드려요! 안전하고 편리한 금융 생활을 만들어가도록 최선을 다하겠습니다.😀

written by Gyuree
edited by Yeojeong


도전적인 개발자와 금융 생활의 혁신을 이끌고 싶다면?