아키텍처는 지도의 축적과 같습니다. 높이 날면 멀리 보지만, 낮게 날면 자세히 본다고 합니다. 더 높은 곳에서 아키텍처를 조망할수록 더 넓게 볼 수 있지만, 자세히 보기는 어렵습니다.


DEVIEW 2023에서 우리 팀이 만들고 있는 사이트 빌더, nBilly의 아키텍처 지향점을 소개했습니다.

이 그림은 우리 시스템을 가장 높은 곳에서 조망하며 깔끔하게 완성된 건물을 보여주는 듯합니다. 너무 당연한 이야기지만 이런 상위 아키텍처만으로 개발 중에 만나는 모든 문제를 해결할 수는 없습니다. 상위 정책이 미치지 못하는 현장이 생기기 마련입니다.
정책이 미치지 못하는 현장을 오래 방치하면 어떻게 될까요? 정책의 부재는 일관성을 훼손시킵니다. 일관성 없는 시스템은 혼란을 초래합니다. 자연스레 팀의 협업 역량을 떨어뜨려 생산성에 좋지 않은 영향을 미칩니다. ‘레고 같은 아키텍처’를 지향하는 팀에게 이런 상황은 치명적입니다.
이 글은 DEVIEW 발표 이후에 우리 팀이, 상위 정책이 미치지 않아 망가진 현장을 발견하고 협력적 아키텍처 설계로 문제를 해결한 과정을 소개합니다.
1. 문제의 발견
디자인 도구나 사이트 빌더의 오른쪽 패널은 마치 글로벌 표준인 듯, 개체의 프로퍼티를 나열하고 프로퍼티의 상태를 변경하는 인터페이스를 제공하는 영역으로 쓰입니다. 우리 팀이 개발하고 있는 nBilly도 다르지 않습니다.

nBilly 사용자는 오른쪽 패널에서 Link 프로퍼티에 버튼 클릭 시 이동할 URL을 설정할 수 있습니다. 그동안 단순히 URL만 입력할 수 있었던 Link 프로퍼티의 기능을 확장해 레이어 팝업도 띄울 수 있게 해달라는 요청을 받았습니다.

왼쪽에서 오른쪽으로 확장하는 간단한 작업입니다. 누구나 쉽게 만들 수 있는 흔한 UI 지만 우리 팀은 이 문제를 좀 더 잘 풀어야 합니다. 우리가 만든 코드는 레고 블록처럼 모듈화되어 미래의 나, 동료, 그리고 더 나아가 고객의 문제 해결에 기여할 수 있어야 하니까요.
손 안 대고 코풀고 싶습니다. 그동안 여러 유형의 프로퍼티를 만들며 기반 코드를 모듈화해서 부품으로 모아왔으니 부품 몇 개 조립하면 금방 만들 수 있으리라 기대합니다.
하지만 이 생각은 십 리를 가지 못합니다.
.
.
.
1차 막힘: 참고할 문서가 없다!?
먼저 부품을 확인할 문서를 찾지 못하였습니다. 물론 디자인 파일에서 UI 구성을 확인할 수 있지만, 지금 필요한 건 디자인 스냅샷이 아닌 분류 체계입니다. 내가 작업할 공간이 어떤 계층 구조와 결합 체계를 지향하는지, 그 안에서 내가 어떤 부품을 사용할 수 있는지 알아야 빠르게 결합 방식을 구상할 수 있습니다.
2차 막힘: 규칙을 읽을 수 없다!?
문서를 찾을 수 없는 상황은 흔히 만나는 일이니, 이 정도는 괜찮습니다. 대신 코드를 읽어서 계층 구조를 파악하기로 합니다. LinkProperty
의 컴포넌트 렌더링 코드를 보고 의존성을 따라가며 참조 패턴을 유추합니다.
<PropertyView undefinedChildrenCase={LinkPropertyView} customPropertyView={children} {...props} />;
<Box mb={2}>
<PropertyInputView {...inputProps} />
</Box>
<Box w={width ?? 'full'}>
{title && <PropertyTitle title={title} />}
<Flex justify="space-between" alignItems="center" border="none" borderColor="inherit">
{label && <PropertyLabel title={label} />}
<Tooltip
variant="invalid"
hasArrow
label={errorMessage}
isDisabled={valid}
offset={[0, 10]}
placement="bottom-start"
>
<InputGroup w={label ? '128px' : '100%'} minWidth={minWidth}>
<PropertyInput
pr={inputRightPadding}
variant="outline"
placeholder={placeholder}
_placeholder={_placeholder}
value={value}
valid={valid}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
{copyable && <CopyButton value={value} />}
<StatusButton {...statusAndRemoveProps} />
</InputGroup>
</Tooltip>
</Flex>
</Box>
컴포넌트 간 참조 방향을 그림으로 그려봅니다.

계층 체계가 선명하진 않지만, LinkProperty
-> LinkPropertyView
-> PropertyInputView
로 이어지는 패턴은 보이는 것 같습니다. 하지만 여기에서 의문이 생깁니다. LinkPropertyView
와 PropertyInputView
는 같은 계층일까요, 다른 계층일까요? 왜 둘의 이름에만 -View
라는 Postfix가 붙어 있는 걸까요? 의도를 알기 어렵습니다. 다른 프로퍼티도 뒤져봅니다.
3차 막힘:일관성이 없다!?
다른 프로퍼티를 살펴보면 규칙을 유추할 수 있을까, AnimationProperty
를 들여다봅니다.

AnimationPropertyView
는 RangeInputProperty
를 의존합니다. 그렇다면 -Property
는 부품일까요, 결합의 결과일까요? 앞서 LinkProperty
의 PropertyInputView
는 PropertyLabel
과 PropertyTitle
등을 직접 의존했지만, 이번에는 PropertySelectInputView
가 PropertyLabel
과 PropertyTitle
을 의존하지 않습니다. 정보를 얻고 싶었지만 일관성 없는 모습을 보고 오히려 더 혼란해졌습니다.
.
.
.
코드까지 읽어봤지만 진실을 찾지 못했습니다. 코드에서 의도를 읽을 수 없다는 건 아키텍처의 품질이 좋지 않다는 신호입니다. 설계 의도가 모호한 아키텍처는 UX가 좋지 않은 서비스와 같습니다.
2. 왜 이렇게 되었을까?
핑계를 대보자면 처음 아키텍처를 설계하던 때에는 우리에게 정보가 부족했습니다. 앞으로 어떤 프로퍼티를 만들어야 할지 잘 몰랐으니까요. 기능이 많이 없었기에 요구사항도 단순했습니다. 단순한 설계 지침만으로 충분히 문제를 해결할 수 있습니다.
처음에 우리 팀은 프로퍼티를 VAC 패턴으로 만들자는 느슨한 합의만 했습니다. VAC Pattern은 로직과 JSX 의존성(표현)을 분리해서 병렬로 개발하거나, 각각을 독립적으로 재사용하기 좋은 아키텍처를 제안하는, React 컴포넌트 디자인 패턴입니다. VAC(View Asset Component)는 JSX로 UI만 표현하고, 비즈니스 로직은 모두 부모 컴포넌트에 맡깁니다.

처음에는 ‘UI 표현과 로직을 적절하게 잘 분리하자’ 정도의 생각만 공유하면 충분했습니다. LinkProperty
와 LinkPropertyView
만 있으면 다 표현할 수 있었습니다. 하지만 시간이 지나며 사정이 변합니다. 도구의 기능이 많아지며 프로퍼티도 다양해졌습니다. 단순하고 느슨하지만 ‘유용했던 그때의 생각’은 이제 더 이상 유효하지 않습니다. LinkPropertyView
아래에 우리가 예측하지 못한 세상이 있었습니다.

LinkPropertyView
의 아랫부분은 설계 정책을 논의한 적이 없습니다. 정책이 없거나 모호한 지점으로 각자의 해석이 들어옵니다. VAC 컴포넌트 뒤에 붙이기로 했던 -View
접미사가 여기저기 퍼지며 남발됩니다. 규칙 없이 코드가 마구 만들어지고 리뷰를 통과하는 코드가 쌓입니다. 비즈니스 요구가 긴박해지고 코드 품질을 한시적으로 타협하던 시기를 거치며 부채가 급격하게 쌓입니다.
시스템에 넓게 퍼진 이런 문제는 혼자 해결하기 어렵습니다. 밑 빠진 독에 물 붓기가 되곤 합니다. 어떻게 하면 좋을까요?
3. 협력적 문제 해결
‘아키텍트로 가득 찬 팀을 만드는 아키텍처 설계 워크숍’이란 글에서 저는 아키텍처를 다음과 같이 정의했습니다.
목표를 달성하기 위해,
주어진 환경을 고려하여(환경),
구성원이 시스템에 가하기로 합의한(사회),
기술적 제약(기술)
아키텍처란 ‘구성원이 시스템에 가하기로 합의한 제약’입니다. 제 아무리 대단한 기술이라 하더라도 다음의 조건을 만족하지 못하는 아키텍처는 잘 동작하기 어렵습니다.
- 나만 알고 있다.
- 알지만 동료에게 충분히 설명할 수 없다.
- 설명할 수는 있지만 동료가 따르게 할 수 없다.
- 따르더라도 문제를 해결하여 목표를 달성할 수 없다.
아키텍처 일관성은 대개 구성원의 다양한 관점이 잘 통합되지 않았을 때 훼손됩니다. ‘서로 다른 생각’은 발 없는 말 같아서 빠르게 전체 코드로 확산됩니다. 넓게 퍼진 문제는 혼자 힘으로 해결하기 어렵습니다.
혼자 할 수 없다면 동료와 힘을 합쳐야겠죠. 동료의 힘을 빌리려면 먼저 문제에 대한 공감대를 만들어야 합니다. 공감대를 만들려면? 문제를 알려야겠죠. 문제 해결은, 나를 도와줄 수 있는 사람에게 문제를 알리는 걸로 시작해야 합니다.
3-1. 문제 시각화하기
제일 먼저 내가 느끼는 불편함을 팀에 알렸습니다. 동료들이 반응했고 상황을 함께 파악하기로 했습니다. 재택근무를 하고 있는 우리 팀은 Zoom에 모였습니다. 먼저 현재 상태를 시각화해서 상황을 파악합니다. 엉켜 있는 코드만 봐서는 문제를 제대로 이해하기 어려울 때가 있으니까요. 시각화로 문제를 더 선명하게 드러낼 수 있다면 더 쉽게 동료와 공감대를 만들 수 있습니다. 참고로 우리 팀은 시각화 도구로 Miro를 사용합니다.
1) 일관성 없는 UI 계층
계층의 일관성이 깨진 부분을 찾기 위해 여러 프로퍼티 중 대표격인 Box 프로퍼티와 Image 프로퍼티가 UI를 바라보는 관점을 그려서 비교했습니다.

사실 너무 쉽게 첫 번째 문제를 찾았습니다. Box 프로퍼티와 Image 프로퍼티가 ‘Property’라고 부르는 대상이 서로 다릅니다. Image 프로퍼티가 먼저 생기고 디자인이 여러 번 바뀌면서 Box 관련 속성을 하나로 모은 Box 프로퍼티 개념이 생기면서 PropertyGroup
이 생겼습니다. 하지만 이 변화는 팀 전체에 충분히 전파되지 못하였습니다. ‘이 사실’을 아는 사람과 모르는 사람이 만드는 코드가 다른 모습으로 자랐습니다. 이 차이는 바로잡을 시점을 놓치고 방치되었습니다. 계층의 가장 꼭대기에서 무너진 일관성은 하위 UI 컴포넌트까지 그대로 전파됩니다.
2) 부족한 계층 추상화

위의 그림에서 보이듯 Box 프로퍼티와 Image 프로퍼티는 모두 동일한 Opacity 컨트롤을 가지고 있습니다. 하지만 Box 프로퍼티는 Opacity 컨트롤을 Property로, Image 프로퍼티는 인라인 된 자식 컴포넌트로 표현합니다.
<Box 프로퍼티의 OpacityPropertyView 컴포넌트>
function OpacityPropertyView({ label, opacity, mixed, onUpdate }: IProps) {
const percentageNumber = toFixedNumber(opacity * 100);
return (
<Box w="full" mt={2}>
<Flex justify="space-between" alignItems="center">
<PropertyLabel title={label} />
<RangeInputProperty
range={DEFAULT_OPACITY_RANGE}
numberInput={percentageNumber}
mixed={mixed}
percentaged={!mixed}
onUpdate={onUpdate}
/>
</Flex>
</Box>
);
}
export default OpacityPropertyView;
<Image 프로퍼티 일부 코드>
{opacity && (
<Flex justify="space-between" alignItems="center">
<PropertyLabel title="Opacity" />
<RangeInputProperty
range={DEFAULT_OPACITY_RANGE}
disabled={!imageForm.src}
numberInput={toFixedNumber(opacity.value * 100)}
percentaged
onUpdate={opacity.onChange}
/>
</Flex>
)}
같은 UI 지만 조립하는 모양이 다르고 다루는 계층도 다릅니다. 두 프로퍼티의 불일치는 코드까지 이어져 중복을 만듭니다. 계층이 충분히 추상화되지 못했음을 알리는 냄새입니다.
3) 거꾸로 흐르는 의존성
AnimationProperty
는 아예 의존성이 반대로 흐르는 지점도 있습니다. 의존성이 일관성 있게 계층의 위에서 아래로 흐르지 않으면 코드를 읽는 사람이 결합의 방향을 파악하기 어렵습니다. 당연히 ‘아래 부품을 조립해서 더 큰 것을 만든다’라는 계층형 아키텍처의 목표를 달성하기도 어렵겠죠.

.
.
.
시각화를 했더니 문제를 선명하게 드러낼 수 있었습니다. 문제가 선명하면 어렵지 않게 공감대를 만들 수 있습니다. 아마 다들 개발하면서 불편함을 느꼈을 테니 어느 정도는 문제를 감지는 하고 있었을 겁니다. 다만 ‘늑대가 나타났다’라고 외쳐야 하는 시기를 놓쳤을 뿐.
3-2. 가설 세우기
문제를 확인했고 공감대도 만들었으니 이제 ‘해결책’을 찾아야 합니다. 일관성을 갖춘 계층을 만들려면 프로퍼티를 구성하는 부품을 ‘빠짐없이 표현하면서도 의도가 서로 겹치지 않는 계층’을 발견해야 합니다. 무엇이 나은 방법인지 아직은 알 수 없기에 다양한 생각을 발산해서 많은 대안을 탐색하는 게 좋습니다. 논의에 참여한 동료와 각자의 방식으로 계층을 만들어서 비교해 보기로 했습니다. 이렇게 하면 더 빠르게 여러 대안을 탐색할 수 있습니다.


여러 대안을 탐색하며 계층 안에 레이어를 넣고 빼기를 반복하다, 결국 우리에게 필요한 계층은 3단계라고 결론을 내렸습니다. 각 층의 이름을 두고 또 고민에 빠졌지만 더 좋은 이름이 떠오를 때까지 Control
, Attribute
, Property
로 부르기로 잠정 합의했습니다. 그리고 각 층의 책임을 분명하게 정의해서 서로 다른 해석이 들어올 여지를 최대한 줄였습니다.

Property
: 서로 관련 있는Attribute
의 집합이자 오케스트레이션 레이어Attribute
: 프로퍼티를 구성하는 하위 속성으로Control
과Label
을 패키징하는 경계Control
: 사용자가 실제로 상호작용하는 버튼, 텍스트 박스 등의 UI 요소
(글을 쓰며 생각해보니, PropertyGroup
, Property
, Control
이 더 나았을 것 같다는 생각이…!)
드디어 콘셉트를 얻었지만 아직은 가설이고 희망 사항입니다. 이 가설이 정말 통할지는 확인해 봐야겠죠.
3-3. 페이퍼 프로토타입으로 검증하기
실제 코드를 작성해서 아키텍처를 검증하려면 비용이 많이 듭니다. 개발자는 너무 익숙해서 잘 인지 못하지만, 코딩은 비싼 작업입니다. 코딩을 하기 전에 간단한 그림으로 아키텍처를 그려보면 더 적은 비용으로도 다양한 테스트를 할 수 있습니다.
가설을 찾을 때와 동일한 방식으로 진행하되 문제 범위를 Animation 프로퍼티 전체로 확장합니다.

다행히 새로운 계층은 Animation 프로퍼티에도 잘 들어맞습니다. Attribute 계층을 보면 해당 프로퍼티가 제공하는 속성을 직관적으로 파악할 수 있습니다. 하지만 Animation 프로퍼티는 구성이 단순해서 UI 복잡도가 낮습니다. 가장 많은 속성을 가진 RichText 프로퍼티와 디자인이 독특한 Padding 프로퍼티까지 추가로 검증해 봅니다.


잘 들어맞습니다. 여기까지 오면 이제 새로운 아키텍처로 문제를 충분히 해결할 수 있겠다는 생각이 강해집니다. 하지만 아직 그림일 뿐입니다.
3-4. 코드로 검증하기
페이퍼 프로토 타이핑만으로는 동료와 내 생각이 일치하는지 확신하기 어렵습니다. 그림을 그릴 때 안 보였던 문제가 코드를 작성하는 중에 드러나기도 합니다. 코드는 개발자에게 더 구체적인 정보를 요구합니다. 샘플을 골라 실제로 리팩터링을 해서 비교하면 서로의 생각을 더 구체적으로 비교할 수 있습니다.
동료와 Padding 프로퍼티를 샘플로 골라서 리팩터링을 하고 코드 리뷰를 했습니다. 역시나 서로 다르게 생각하는 부분을 찾을 수 있었습니다. 이 틈은 코드 리뷰를 거치며 매워집니다. 생각의 거리를 좁히며 아키텍처는 더 탄탄해집니다.



3-5. 작업하기
가설을 세우고, 프로토타입을 만들어 가설을 검증했습니다. 코드 리뷰를 거치며 동료와 생각의 차이를 좁혔습니다. 이제 새로운 규칙을 전체 시스템에 적용할 일만 남았습니다. 이후에는 협업 강도를 조금 떨어뜨리고 작업 목록을 도출한 후 각 담당자가 병렬로 작업을 진행했습니다. 그래도 확신은 금물이지만 코드 리뷰라는 안전장치가 있습니다. 작업 중 만나는 문제는 허들에서 빠르게 논의하여 코드 리뷰까지 끌고 가지 않기로 했습니다. 코드는 비싼 자원입니다. 코드를 작성하기 전에 문제를 해결할 수 있다면 더 좋습니다.

4. 다시 되찾은 UI 계층 일관성
‘문제 시각화하기 → 가설 세우기 → 페이퍼 프로토타입 검증하기 → 코드로 검증하기’ 과정을 거치며 아키텍처의 훼손된 일관성을 복원하였습니다. 아래 그림은 아키텍처 개선 전과 후의 차이를 보여줍니다.

왼쪽의 AS-IS 버전은 계층 구조를 직관적으로 표현하지 못합니다. 분명하지 않은 규칙이 만든 혼란은 코드까지 전파되어 설계 의도를 이해하기 어렵게 만들었습니다. 이에 반해 오른쪽의 TO-BE 버전은 Property
-> Attribute
-> Control
로 이어지는 계층의 경계와 규칙을 더 선명하게 보여줍니다. 새로운 아키텍처는 UI 스펙을 훨씬 더 직관적으로 표현합니다. Attribute
만 봐도 제공하는 속성을 유추할 수 있습니다.
더 나아가 리팩터링을 하면서 원하는 부품을 빠르게 탐색할 수 있는 카탈로그를 만들었습니다. 이제 새로운 프로퍼티를 만들 때, 이 카탈로그에서 계층 구조를 파악하고, 계층별로 필요한 부품을 찾아서 조립 방향을 구상할 수 있습니다.

5. ‘협력’이 중요한 아키텍처 설계
우리 팀은 문제를 해결하는 과정에서 ‘협력’을 강조했습니다. 협력적 아키텍처 설계의 중요성을 경험으로 터득해왔기 때문입니다. 새로운 해결책을 만들었어도, 해결책에 담긴 생각이 팀 전체에 충분히 전달되지 않는다면 일관성은 금세 또 깨져버립니다.
다양한 이해관계가 얽히는 팀 프로젝트에서, 아키텍처 전체에 넓게 퍼진 문제를 혼자만의 힘으로 해결하기 어렵습니다. 개인이 겪는 혼란을 팀의 문제로 인식하고, 모두가 힘을 모아 튼튼하게 뿌리내릴 수 있는 해결책을 만들어야 합니다. 아키텍처 설계는 환경과 기술은 물론 사회적 요인까지 다뤄야 하는 어려운 일입니다.
협력적 아키텍처 설계는 구성원의 다양한 관점과 지식을 이끌어내 개별 지식 격차를 줄입니다. 무엇보다 문제 해결과 정보 전달이 동시에 이뤄지는 환경을 제공하여 생각을 더 높은 밀도로 전달할 수 있게 돕습니다. 잘 공유된 생각이 더 좋은 협업 품질을 만들고, 더 좋은 협업 품질이 더 좋은 아키텍처로 우리를 이끈다고 믿습니다.