Product Language

14454 단어 아티클아티클

Product Language란?

Design System VS Product Language
일반적으로 디자인 시스템이 디자인에 한정된 내용을 다루는 경향이 있다면, Product Language는 제품을 만드는 구성원 모두가 공유하고 사용하며 만들어가는 언어라고 할 수 있다.
(즉, Product Language는 서비스를 만드는 모두가 하나의 요소에 대해 부르는 공통적인 언어라고 할 수 있다.)

왜 Product Language가 필요할까?

여러 파트와의 협업시, 나타날 수 있는 상황 중 서로 비슷한 대상을 다른 이름으로 부르고, 심지어는 다른 대상을 같은 이름으로 부르는 경우까지도 발생하니 회의 시간의 첫 30분이 “용어정리”에 고스란히 할애되는 상황은 종종 발생할 수 밖에 없다.

이 상황에서의 해결책은 명확하다.
문제의 원인이 공통적인 UI에 대해서 서로 다르게 말하고 있는 것이기 때문에 이를 통일시키면 된다. 제품을 만드는 모든 구성원이 UI를 추상화하는 규칙 및 단위를 공유하는 것이다.

어떻게 만들면 될까?

디자이너, 클라이언트(앱/웹)이 모여서 하나의 통일된 규칙을 만드는 것에서부터 잘 안맞을 수 있다. (애초에 의사소통이 원활하게 잘 되었다면 이러한 규칙을 만들 필요도 없었을 것이다.)

뱅크샐러드에서는 이러한 Product Language를 따로 만들어 BPL로 명칭하고 있다. 이들이 BPL을 만들 때 고려했던 두가지를 정리하자면 다음과 같다.

뭘 만들어야 할지 모르겠을 때는, 일단 만들어봅시다.

Communication cost is most expensive.
Code and Show first, argue after that.

확실한 의사소통을 한 뒤에 뷰와 기능이 확정되고 개발이 들어가면 좋겠지만, 앞서 말했던 상황과 같이 그렇지 못할 상황이 더 많다. 그러므로 "일단" 만들어보는 것이다.

이런 자세가 의사소통해서 한 번에 하면 되는 일을 불명확한 의사소통 위에서 두 번 일하는 것처럼 보일 수도 있다.

하지만 확실하게 의사소통하는 일은 매우 어렵고 심지어는 가능할지조차 불확실한 경우가 많다. 이런 불확실성을 줄일 수 있는 가장 빠르고 강력한 방법 중 하나가, 내 코드의 대부분이 버려질 것을 각오하고서라도 어떻게든 돌아가는 제품을 만드는 것이다.

각 플랫폼 별로 이슈가 되는 화면/기능에 대해 일단 만들어오고 나서 의논을 해보면 그들이 각자 가져온 코드의 비교를 통해 어디까지 서로 같은 개념을 공유하고 있고, 또 다르게 생각하고 있던 부분은 어디였는지 명확히 알 수 있다.

이런 방식으로 코드를 비교하면 “결국 우리가 합의해야 하는 것은 A가 아니라 B구나!”라는 것을 알 수 있었고, 회의의 목적이 분명해지고 보니 회의 역시 빠르게 진행이 될 수 있다.

말로만 의사소통 하다 보면 서로 비슷한 이야기를 하고 있다고, 심지어 서로를 모두 잘 이해하고 있다고 착각하기 쉽다. 오직 코드로 제품을 만들고 나야만 가시화되는 이해의 간극들이 있고, 그런 간극들이 제품을 통해서만 좁혀지는 경우도 있다.

서로의 도구를 사용해 봅시다

개발자들이 디자이너의 작품을 볼 수 있는 환경은 여러가지가 있다. 그중에서 아마 가장 많이 사용하고 있는 툴 중 하나가 Zeplin일 것이다.

Zeplin을 통해 디자이너의 작업을 본다면 보다 빠르게 화면의 구성과 요소들을 파악할 수 있지만, 디자이너의 숨은 노력과 수고를 미처 보지 못한다. Zeplin은 개발자 입장에서 디자이너들이 고민하는 세부적인 내용들을 가려주는 Facade(외벽) 역할을 하는 것이다.

뱅크 샐러드에서는 이 문제를 해결하기 위해 Figma 툴을 도입했고 개발자들에게 단순히 Viewer의 권한을 주는 것이 아니라, Editor의 역할까지 주는 것으로 작업 환경을 구성했다.

디자이너들이 쓰는 도구를 사용해보고 관련 튜토리얼들을 따라해보며 실제로 그들이 어떤 기능을 써서 각 컴포넌트들이 어떤 관계를 가지도록 의도하며 만들었는지를 확인하는 것이다. 이런 과정을 거치면 이전에는 보이지 않았던, 비로소 눈에 보이는 것들이 있다.

예를 들면 Figma의 컴포넌트가 Class단위의 추상화와 얼마나 유사한지, 또 Autolayout이라는 개념이 iOS의 UIStackView 또는 Android의 LinearLayout과 얼마나 유사한지 등 이다. 이런 부분들에 대해 약간의 이해가 생기면 개발자-디자이너간 의사소통에는 급격하게 속도가 붙게 된다.

그렇다면 실제로 어떻게 구현할까?

뱅크 샐러드에서 위의 두가지의 원칙을 통해 BPL을 만들고 나니 개발자들이 할 일이 보다 명확해졌다.

디자인 도구들을 어느 정도 만지작 거리자, 개발차원에서 무엇을 만들어야 하는지 알 수 있게 되었다. 바로 디자이너가 컴포넌트를 만들면 1) 그것이 만들어진 방식을 최대한 모방하여, 2) 그것과 1대 1로 대응되는 클래스를 만드는 것이 개발자들의 할 일이었다.

최대한 Flat하게

성급한 추상화를 주의해야한다.

내부적인 구현의 경우, 디자이너가 만든 방식을 최대한 모방해서 작업하는 것이 중요하다. (-> 추후 Figma에서 디자이너가 수정한 만큼만 다시 수정할 수 있도록 ex. 디자이너가 AutoLayout을 쓰면 UIStackView를 쓰고 Constraints를 쓰면 NSLayoutConstraint를 써서 구현하는 것)

이렇게 작업을 진행하게 되면 어쩔 수 없이 복사/붙여넣기의 작업이 많아질 수 밖에 없다. 그러나, 지금 당장 해당 컴포넌트가 비슷해보일지라도 "Figma에서 서로 의존성이 없는 독립적인 컴포넌트라면 코드레벨에서도 서로 의존성이 없는 독립적 요소"여야 한다.

뱅크 샐러드의 경우,

이러한 두가지 컴포넌트에 대해 비슷하게 보일지라도 하나로 추상화하지 않고

다음과 같이 따로 작업을 진행하였다.

최대한 SwiftUI처럼

뷰의 위계 관계
SwiftUI의 여러 대표적 특징 중 하나가 바로 코드에서 위계가 보인다는 것이다. 이를 참고해서 UIKit를 구현하면 Figma에서 보이는 컴포넌트의 위계관계를 그대로 코드에 반영할 수 있다.

VStack, HStack과 같이 레이아웃에 대해서만 책임을 지는 Container컴포넌트들과, Text나 Button 과 같이 실제로 컨텐츠를 표현하는 Child컴포넌트로 나누는 것이다.

List.Section10(header: mainHeader(for:sectionData), collection: viewModel.subSections) { subSectionData in
    List.Margin0(header: subHeader(for: subSectionData), collection: subSectionData.assets) { item in
        ListItem.small1 {
            ListItem_LeftImageText()
                .then { $0.image = item.image }
                .then { $0.text = item.name }
            ListItem_RightTextMedium1()
                .then { $0.text = item.amountText }
        }
    }
}

뱅크 샐러드에서는 위와 같이 작업을 하였고 이 코드를 보면 실질적으로 SwiftUI와 거의 똑같은 Syntax로 코드를 짜는 것이 가능했고 그 결과 아주 쉽게 Figma에서 보여지는 위계를 코드로 그대로 표현이 가능했다.

데이터 변화에 따른 뷰의 변화
SwiftUI의 대표적 특징 중 하나는 바로 View에서 표현되어야 하는 정보들을 상단부에 모아놓고, 이 정보를 담은 변수들을 @State로 표시하면, SwiftUI의 엔진이 이 변수들에 변화가 생길 때마다 “레이아웃이 더러워졌다(dirty the layout)“ 라고 판단해서 View를 처음부터 다시 그리는 형태이다.

이를 UIKit에서는 LayoutDrivenUI 개념을 사용해서 구현할 수 있다.

SwiftUI

struct ContentView: View {

    // @State로 annotate된 변수에 변화가 생기면 뷰를 처음부터 다시 그립니다.
    @State var text: String = "HI"
    @State var image: UIImage = UIImage(systemName: "circle")!

    var body: some View {
        HStack {
            Image(uiImage: self.image)
            Text(self.text)
        }
    }
}

UIKit

class ContentView_: UIView {
    var text: String = "HI" { // 표현해야 할 정보들에 변화가 생기면 레이아웃을 더럽힙니다.
        didSet { setNeedsLayout() }
    }
    
    var image: UIImage = UIImage(systemName: "circle")! {
        didSet { setNeedsLayout() }
    }
    
    private lazy var imageView = UIImageView()
    private lazy var textLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        layout()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // layout을 다시 하는 layoutSubView안에서 상태를 최신화 합니다.
        imageView.image = image
        textLabel.text = text
    }
    
    func layout() {
        addSubViews([imageView, textLabel])
        // 레이아웃 초기화 코드는 초기화 메소드 안에서만 불립니다.
    }
}

여기에 더해, SwiftUI는 @State만으로 작동되는 것이 아니라, 상태가 변화함에 따라 이를 다시 binding하는 과정을 거치고 있다. Combine이라는 비동기적 이벤트 전달방식을 통해 새로운 정보를 업데이트 하는 것이 더 일반적이다.

이를 UIKit에서는 RxSwift라는 툴을 사용해서 해결할 수 있다. RxSwift를 활용해 ViewModel을 만들고, 이 ViewModel의 정보가 바뀔 때마다 BPL컴포넌트를 업데이트 시키는 방식으로 SwiftUI를 흉내낼 수 있다.

Sample App - IntegralExample

두가지 규칙을 모두 부합하는 하나의 앱인 IntegralExample을 만드는 것에 대해서, 몇몇 사람들은 비효율적인 일이라고 할 수 있다.

이런 식으로 샘플 앱을 만들게 되면 UI코드는 이 IntegralExample 앱에서 먼저 작성되어 테스트되고, 실제 뱅크샐러드 앱에는 그 코드들이 거의 그대로 복사되게 된다.

그 결과, 추후에 뱅크샐러드 앱에서 UI관련한 버그가 발생했을 때, 대부분의 경우에는 IntegralExample에서 바로 재현이 가능하고 따라서 IntegralExample에서 수정을 마친 뒤, 해당 수정을 뱅크샐러드 앱이나 BPL컴포넌트에 적용하는 방식으로 작업이 진행된다.

그러므로, 수정사항이 생겨 변경을 하게 되었을 때 2~3분의 빌드가 되어야 하는 일을 빠르게 수정만 하고 바로 적용할 수 있을 뿐 아니라, 무엇보다 개발자와 디자이너의 소통의 창을 보다 수월하게 만들어준다는 장점이 있다.

  1. 디자인 시안에 나와있는 컴포넌트의 이름을 Xcode에서 검색하면 그 컴포넌트의 구현체를 바로 발견 할 수 있고
  2. 그 컴포넌트를 디자인 시안에 나와있는 방식 그대로 구성하면 UI가 완성되며
  3. 그 UI를 확인하는데 까지 드는 시간이 10배 이상 짧아진 것이다.

좋은 웹페이지 즐겨찾기