오늘은 일관된 아키텍처를 설계하는 것이 함께 개발할 때의 코드 퀄리티에 얼마나 중요한지 체감했던 경험과 함께, 그 경험을 돌아보며 시도했던 것 중 아쉬웠던 부분을 다음엔 어떻게 개선할 수 있을지에 대해 고민해본 내용에 대해 정리해보려고 합니다.
주니어 개발자로 입사해 개발을 하면서 아키텍처 설계에 대한 중요성을 많이 체감했습니다.
여러 팀원이 함께 협업하면서 개발하는 만큼 적절한 아키텍처가 사전에 잘 협의되는 만큼 이후의 코드리뷰와 개발도 수월해지기 때문입니다. 달리 말하면, 사전에 그렇지 못했기 때문에 나중에 그 배가 되는 시간을 써야 했었던 경험들도 있습니다. 🥲
팀원들과 함께 급한 일정에 쫓겨 빠르게 개발하려던 어느 시기가 생각나네요.
주니어들끼리 머리를 맞대며 나름대로 api형태도 협의하고, 컴포넌트 구조도 협의해 업무 분담을 진행한 뒤 코드리뷰를 시작했을 때의 그 아찔함과 당황스러움이 기억이 납니다. ㅋㅋㅋ
너무 큰 틀에서의 협의만 한 탓에 다른 프론트엔드 팀원이 생각하고 써내려간 구조는 제 예상과 사뭇 달랐고, 서로 미처 생각하지 못한 부분들도 있었습니다. PR에서 리뷰를 한 뒤 이를 모두 바꾸자니 팀원의 입장에서는 시간이 없고, 결국 최소한의 필수적인 부분을 제외하고는 '하면서 다시 리팩토링하자'라는 방향으로 결론을 내려 개발을 진행해나갔습니다.
그리고 테크리드의 앞에서 중간 오프라인 코드리뷰를 하게 되었을 때 우리는 산산조각이 났습니다.^^
그 이후로는 전반적인 플로우를 시퀀스 다이어그램으로 그려 먼저 리뷰하고, 아키텍처부터 협의해 리뷰하는데 훨씬 많은 시간을 쏟았습니다. 그럴수록 설계에 대한 중요성을 많이 체감했습니다.
그러나 어떤 프로세스를 거쳐 협업할 것인가에 대해서는 다소 막막한 부분들이 여전히 있었습니다.
오늘은 이 경험을 토대로 당시의 문제들을 짚어가며, 어떻게 개선할 수 있었을지에 대한 이야기를 해보겠습니다.
저는 당시 근무하던 회사에서 3d 그래픽으로 오브젝트를 수정 및 변형 작업을 할 수 있도록 인터페이스를 지원하는 기능을 추가하는 작업에 참여하게 되었습니다. 해당 영역은 이미 구현되어 운영중이었고, 특정 영역을 추가하거나 변경 및 확장하는 것이 태스크의 일부였습니다. 이전에는 참여하지 않았던 영역이었으나 중간에 투입되었습니다.
작업할 영역은 다양한 dropdown, slider등의 인풋 ui를 가진 하위컴포넌트가 블록처럼 조립될 수 있는 형태로, 빌더 페이지에서 많이 볼 수 있는 UI를 갖고 있었습니다.
쉽게 이해할 수 있도록 유사한 UI를 가진 예시를 가져와봤습니다.
이미지 출처: nBilly (망가진 UI 아키텍처 일관성 '함께 고치기')
이를 위해 먼저 기존 구조를 파악하고, 요구조건대로 구현해 추가해야 했습니다.
처음에는 쉬운 태스크가 될 거라고 생각했습니다.
세분화된 하위 컴포넌트를 조립해 새로운 상위 컴포넌트를 만들어주고, 필요에 따라 변경하면 그만이라고 생각했기 때문입니다.
..아니었습니다.
어떤 문제가 있었냐면요,
전체적인 구조는 크게 다음과 같았습니다.
Input 컴포넌트는 공통적으로 포함해야 하는 name property를 인라인된 요소로 각각 갖고 있었기 때문에, 재사용성과 유지보수성이 떨어졌습니다.
//InputA
<div className={styles.container}>
<div className={styles.label}>{name}</div>
<InputA />
</div>
//InputB
<div className={styles.container}>
<div className={styles.label}>{name}</div>
<InputB />
</div>
몇몇 컴포넌트는 다른 컴포넌트의 구조를 따라가지 않았습니다.
아래 예시를 보면, 각각의 큰 타이틀을 기준으로 BaseComponent가 감싸고 그 하위 내용을 Component에서 관리하는 구조로 작성되어 있었기 때문에 Background 카테고리처럼 구분되어 있어야 하지만
Client script와 Server script는 갑자기 합쳐져 버렸습니다.
카테고리 타이틀 또한 Component에서 인라인되어 들어가 있습니다.
function ScriptComponent = () => {
return (
<div className={styles.container}>
<p>Client script</p>
<InputA type="client"/>
</div>
<div className={styles.container}>
<p>Server script</p>
<InputA type="server"/>
</div>
//...
기존에 의도했던 대로 일관적인 아키텍처를 유지하기 위해서는 아래와 같은 구조가 되어야 했습니다.
BaseComponent는 children을 아코디언 형태로 감싼 ui를 제공합니다.
function BaseComponent({title, foldable, children}) {
return (
<div className={styles.container}>
<div className={styles.title}>
{title}
</div>
<div className={styles.accordian}>
{children}
</div>
</div>
그러다보니 나중에 추가된, 아코디언이 아닌 다른 형태의 ui에 유연하게 대응하지 못했고 다음과 같은 구조가 생겨버렸습니다.
function ScriptComponent = () => {
return (
<div className={styles.container}>
<button className={styles.add_button} onClick={() => addUserScript("serverScript")}>
<Plus />
<p>ADD SCRIPT</p>
</button>
<p>Client script</p>
<InputA type="client"/>
</div>
<div className={styles.container}>
<button className={styles.add_button} onClick={() => addUserScript("serverScript")}>
<Plus />
<p>ADD SCRIPT</p>
</button>
<p>Server script</p>
<InputA type="server"/>
</div>
//...
기존의 아코디언 형태를 이미 최상위 컴포넌트가 가지고 있는 상태에서, 다른 형태에 대응할 수 없어 그대로 해당 아이콘이 Component 레벨에서 인라인되어 버렸습니다.
📍 당시 코드의 전반적인 문제점을 하나하나 짚는 것은 그 코드를 비난하기 위함은 아닙니다.
다른 좋은 방법들이 있었겠지만, 아마도 이 부분을 작업한 개발자 분의 상황을 유추해보자면, 다른 바쁘고 중요한 태스크들이 있는 상태에서 해당 ui를 위해 컴포넌트 구조를 리팩토링하는 것은 우선순위에서 많이 밀려났던 것 같습니다. 해당 기능이 기한에 맞추어 릴리즈 되는 것은 충분히 중요하지만, 컴포넌트의 재사용성을 높이고 코드 품질을 유지하기 위해 당장 리팩토링을 하는 것은 이미 운영중인 서비스에 사이드 이펙트가 발생할 리스크를 감수해야 했을테니까요. 각 컴포넌트에 대한 문서화나 테스트가 없는 상태였다보니 더욱 그랬을 것 같습니다.
실무에서는 늘 발생하는 상황인 만큼, 이러한 상황을 가장 효율적으로 해결할 수 있는 방법을 찾는 것이 중요하겠죠!
생각처럼 태스크는 간단히 끝나지 않았습니다.
억지로 우겨넣을 순 있었지만 해당 영역이 회사의 서비스의 중요한 부분이었기 때문에 언젠가는 이 문제를 해결해야했습니다.
더군다나 다음 분기에는 해당 영역의 ui 리뉴얼 작업이 예정되어 있었기 때문에 그 이전에 컴포넌트의 재사용성을 끌어올려야 여러모로 작업효율성 또한 크게 올라갈 것이 분명했습니다.
네, 우리는 압니다. 어떤 점이 문제인지 모두 잘 알고 있고 고쳐야 하는 것 또한 잘 알고 있습니다. 사실 혼자 개발하는 사이드 프로젝트였다면 전혀 어려움은 없었을 거에요!
문제는 이 태스크 말고도 우리는 다른 급하고 중요한 태스크들이 눈앞에 쌓여있다는 것, 혼자 마음대로 고칠 수 있는 것이겠죠.
개선을 위해 필요한 것은 2가지 입니다.
1. 아키텍처: 컴포넌트 구조를 어떻게 변경할 것인지
2. 프로세스: 어떻게 아키텍처를 설계하고, 팀원들과의 협의와 리뷰를 통해 바꾸어 나갈 것인지
바쁜 팀원들에게 이 태스크를 함께 해야 하는 이유에 대해 설득하고 공감을 얻기 위해서는, 현재 구조는 어떻고 그것이 어떤 문제를 야기하는지를 잘 정리해 전달할 수 있어야 했습니다. 처음부터 구현을 하지도 않았기 때문에 모든 컴포넌트의 구조를 잘 파악해 정리하는 것이 필요했습니다.
그 다음엔 문제를 해결할 수 있도록 어떻게 아키텍처를 개선할 것이며 그 아키텍처는 동일한 문제가 발생하지 않는다는 것을 검증할 수 있어야 했습니다.
더불어 어떤 프로세스를 통해 진행할 것인지, 예상 소요시간과 일정 또한 산정이 되어야 팀원들의 리소스를 분배할 수 있겠죠.
오늘 주제와 크게 관련이 없어 위에서 언급하지는 않았지만, 다른 부분들은 함께 리팩토링 해나가는 것에 성공했습니다.
하지만 아키텍처를 개선하는 것은 그 프로세스를 이끌어나가기 어려웠던 것이 사실입니다.
경험이 없는데, 어디서부터 접근하고 어떻게 도움을 받아야할지 감이 오지 않았고 이는 팀원들 또한 마찬가지였습니다.
이런 경우 어떻게 개선해나갈 수 있을까요?
계층은 다음과 같이 정했습니다.
결정한 계층으로 필요한 모든 요구사항에 대응할 수 있는지 검증할 수 있어야 합니다. 같은 방법으로 기존 구조를 정리해둘 수 있고, 이렇게 다이어그램으로 그려 정리해나갈 수 있습니다.
compound component 방식으로 결정했습니다.
compound component는 세분화된 컴포넌트를 조립해 상위 컴포넌트를 만드는 방식에 적합하며, 이를 통해 유지보수와 재사용 측면에서 많은 이점을 가질 수 있습니다. 컴포넌트를 각각의 블록처럼 쌓아 만드는 ui 특성상 적합해 선택했습니다.
컴포넌트 설계 방식에 따라 적절한 폴더 구조를 정의했습니다.
properties 폴더 하위에 controls, properties, propertyGroups 폴더를 두어 각 폴더마다 해당하는 계층의 컴포넌트 명으로 컴포넌트를 관리했습니다.
선언한 계층별로 컴포넌트를 작성했습니다.
이제 최상위 컴포넌트에서는 각각의 PropertyGroup을 볼 수 있습니다.
function BackgroundProperties() {
return (
<S.Container>
<Menu.Title hasGoBackButton>Background Properties</Menu.Title>
<>
<PostProcessing />
<Appearance />
<Environment />
<ToneMapping />
<Material />
<Transform />
<Background />
<Script name="Client script" />
<Script name="Server script" />
</>
</S.Container>
);
}
아래와 같은 property group은 Accordian형태이기 때문에 AccordianGroup을, 기본 Property와 ToggleConotrol을 조합해 만들어집니다.
import React from "react";
import Properties from "@components/properties";
function PostProcessing() {
return (
<Properties.AccordianGroup name="Post Processing">
<Properties.Property name="SSAO">
<Properties.ToggleControl id="SSAO" />
</Properties.Property>
<Properties.Property name="Bloom">
<Properties.ToggleControl id="Bloom" />
</Properties.Property>
</Properties.AccordianGroup>
);
}
export default PostProcessing;
다음과 같은 영역이 추가되어야 한다고 가정하겠습니다.
기존에는 대응이 어려워 인라인 형태로 요소를 넣었고, 그 결과 아키텍처의 일관성이 깨졌습니다. 이러한 문제가 중첩되다보면 개발자들은 상당한 레거시에 대한 부담을 안게 됩니다.
변경된 형태에서는 어떻게 대응할 수 있을까요?
Accordian형태가 아니기 때문에 리스트 형태의 ListPropertyGroup을 추가할 수 있습니다.
function Script({ name }: Props) {
return (
<Properties.ListGroup name={name}>
<Properties.IconInputProperty Icon={BsFileEarmarkPlusFill} name="">
<Properties.PopOverControl items={menus}></Properties.PopOverControl>
</Properties.IconInputProperty>
</Properties.ListGroup>
);
}
마찬가지로, 여러 형태의 ui가 추가되거나 변경되어도 블록을 조립하듯 유연하게 대응할 수 있습니다.
<Properties.ListGroup name={name}>
<Properties.IconInputProperty Icon={BsFileEarmarkPlusFill} name="">
<Properties.SliderControl></Properties.SliderControl>
</Properties.IconInputProperty>
</Properties.ListGroup>
<Properties.ListGroup name={name}>
<Properties.Property name="new">
<Properties.SliderControl></Properties.SliderControl>
</Properties.Property>
</Properties.ListGroup>
앞서 언급했듯, 당시에는 ui 리뉴얼 작업이 예정되어 있었습니다. 리뉴얼의 범위는 알 수 없는 상태였으나 컴포넌트 구조 상 컴포넌트의 재사용성이 떨어져 일일히 변경작업을 해주어야 한다는 것은 충분히 예상가능했습니다. 또한 아키텍처의 일관성이 떨어졌으므로 변경되지 않은 ui가 없는지 일일히 확인하지 않는 이상 확신할 수 없었습니다.
변경된 형태에서는 이를 걱정하지 않아도 됩니다.
기존과 달리 Property에서 Control을 제외한 해당 프로퍼티의 이름 ui를 담당하고 있기 때문에, ui를 한번에 업데이트할 수 있습니다.
function Property({ name, id, children, appearance }: Props) {
return (
<S.Container isVertical={appearance?.isVertical}>
<S.Label htmlFor={id ?? name}>{name}</S.Label>
{children}
</S.Container>
);
}
function Transform() {
return (
<Properties.AccordianGroup name="Transform">
<Properties.Property name="Position" appearance={{ isVertical: true }}>
<Properties.CoordControl />
</Properties.Property>
<Properties.Property name="Rotation" appearance={{ isVertical: true }}>
<Properties.CoordControl />
</Properties.Property>
<Properties.Property name="Scale" appearance={{ isVertical: true }}>
<Properties.CoordControl />
</Properties.Property>
</Properties.AccordianGroup>
);
}
다른 개발팀원이 투입되어 한 부분을 맡게 된다고 해도 문서화가 되어 있기 때문에 빠르게 구조를 파악하고 개발에 착수할 수 있습니다. 또한 코드리뷰 역시 효율적으로 진행할 수 있습니다.
storybook을 사용했습니다.
properties 폴더 하위의 각 계층 controls, properties, groups를 두고 각각의 계층에 해당하는 컴포넌트의 스토리를 넣었습니다.
이제 해당 영역을 작업해야 하는 다른 개발자는 해당 영역을 참고하여 각 컴포넌트의 역할과 모양새, 하위 컴포넌트를 조립해 사용하는 방식을 한 눈에 파악해 빠르게 작업할 수 있습니다.
PR에서 바로 ui를 직관적으로 리뷰할 수 있도록 chromatic을 연동했습니다. 각각의 개발자가 컴포넌트를 작업해 pr리뷰요청을 하면, 코드 뿐 아니라 ui및 동작까지 확인 후 리뷰할 수 있도록 했습니다.
컴포넌트 설계 방식과 아키텍처 설계 과정, 문서화까지 고려해 기존 코드를 어떻게 개선해나갈 수 있을지에 대해 적용한 내용을 정리해보았습니다. 나아가 컴포넌트 추상화와 깔끔한 코드 작성에 대해 앞으로도 정말 많이 공부해야겠다는 생각이 드네요. 개발조직 내에서 필요에 따라 코드 개선을 제안하고 함께 퀄리티를 높여가는 과정은 정말 소중하지만, 그만큼 어떻게 함께 효율적으로 이것을 진행할지도 꼭 고민해봐야 한다는 생각이듭니다. 경험이 없으면, 막상 실무에서 해내기는 쉽지 않아 아쉬움이 있었던 만큼, 공부해나가며 공부한 내용을 어떻게 조직내에서 일하며 적용해 나갈 것인지에 대해서도 많이 찾아보고 배워야 할 것 같습니다.