-
[Frontend] (번역) 미래 지향적인 프론트엔드 아키텍쳐 구축Frontend 2023. 5. 10. 22:51
들어가며
안녕하세요 :)
프론트엔드는 백엔드에 비해 쉽게 UI 변경이 잦고, 기능이 여러 추가됩니다. 이때, 변경할 때마다 쉽게 대응이 가능한 유연한 코드를 짠다면 업무의 양이 줄어들 것입니다..! 마침 변경에 용이한 컴포넌트 짜는 방법에 대해 소개한 글을 발견하여 공유하면 좋을 것 같아 번역글을 작성합니다.
원글: Building future facing frontend architecures
미래 지향적인 프론트엔드 아키텍쳐 구축
1. 소개
성능이 뛰어나고 변경이 용이한 프론트엔드 아키텍쳐를 구축하는 것은 규모가 커질수록 어렵습니다.
이 가이드에서, 많은 개발자들과 팀이 작업한 프론트엔드 프로젝트의 복잡성이 신속하고 조용하게 복합화되는 주요 방법을 살펴보겠습니다.
또한, 이러한 복잡성에 압도되지 않도록 효과적인 방식도 알아보겠습니다.
(문제가 발생하기 전이나 “어떻게 이렇게 복잡해질 수가 있지?”라고 생각을 마주했는데 기능을 추가하거나 변경해야 할때)
프론트엔드 아키텍쳐는 다양한 관점에서 광범위한 주제입니다. 이 가이드에서 특히 변경사항에 쉽게 적용할 수 있는 유연한 컴포넌트 코드 설계에 집중할 것입니다.
여기서는 react를 예시로 설명합니다만, component 기반의 프레임워크라면 똑같이 적용될 수 있습니다.
자 그러면, 먼저 코드가 작성되기 전에 코드의 구조는 어떤 영향을 줄 수 있는지 설명해보겠습니다.
2. 일반적인 정신 모델의 영향
우리가 사물을 어떻게 생각하는지 정신모델은 결국 우리의 결정에 많은 영향을 끼칩니다.
대규모의 코드베이스에서, 많은 의사 결정이 지속적으로 이루어짐에 따라 전체적인 구조가 결정됩니다.
우리가 팀을 통해 무언가를 만들 때, 우리가 가지고 있는 모델을 명시하고, 다른 사람들이 그렇게 생각한다고 기대하는 것은 중요합니다. 왜냐하면 모두 보통 각각 암묵적인 자신만의 생각들을 가지고 있기 때문입니다.
그렇기 때문에 팀은 공유된 스타일 가이드나 prettier 같은 도구가 필요합니다. 그래서 그룹으로써, 일이 어떻게 일관 되는지, 어떠한 것인지, 어디로 가야하는지에 대해 공유된 모델을 가지고 있습니다.
이것은 삶을 훨씬 편하게 만듭니다. 이를 통해 시간이 지남에 따라 모든 사람이 자신의 길을 가는 유지관리하기 힘든 코드 기반으로 내려가는 것을 막을 수 있습니다.
빠른 출시를 바라는 많은 개발자들에 의해 빠르게 개발되는 프로젝트를 경험해봤다면, 적절한 가이드 라인 없이 일이 얼마나 빠르게 통제불능되는 것을 보셨을 것입니다. 그리고 시간이 지남에 따라 더 많은 코드가 추가되고, 런타임 성능이 저하됨에 따라 프론트엔드는 느려질 수 있습니다.
다음 섹션에서, 아래 질문에 대해 답변을 살펴보겠습니다.
1. react 같은 컴포넌트 기반의 모델 프레임워크를 사용하여 프론트 엔드 어플리케이션을 개발할 때, 가장 일반적인 정신 모델은 무엇인가요?
2. 일반적인 정신 모델은 컴포넌트를 구성하는 방식에 어떤 영향을 주나요?
3. 급속한 복잡성을 이끌 수 있는, 명시적으로 만들수 있는 그것에 어떤 트레이드 오프가 거기에 암시되어 있을까요?
3. 컴포넌트로 사고하기
리액트는 가장 유명한 컴포넌트 기반의 프론트엔드 프레임워크입니다. “리액트로 사고하기”는 처음 시작할 때, 보통 처음으로 읽는 아티클입니다.
리액트 방식으로 프론트엔드 애플리케이션을 구축할 때, 그 아티클은 어떻게 생각해야 하는지에 대한 핵심 정신 모델을 제시합니다. 또한, 다른 컴포넌트 기반의 프레임워크에게도 같이 적용되는 조언이기 때문에 좋은 아티클입니다.
컴포넌트를 만들 때마다 다음과 같은 질문을 할 수 있도록 하는 기본 원칙입니다.
- 이 컴포넌트의 단일 책임은 무엇입니까? 좋은 컴포넌트 API 디자인은 합성 패턴에서 중요한 단일 책임 원칙을 자연스럽게 따릅니다. 단순한 것일 수록 융합하기 쉽습니다. 요구사항이 들어오고 변경됨에 따라, (이 글의 뒤에서 보시겠지만), 단순하게 유지하는 것은 꽤 어렵습니다.
- 그 상태에 대한 절대적으로 최소하지만, 완벽한 표현은 무엇입니까? 이 아이디어는 변형을 파생시킬 수 있는 상태에 대해 가장 작지만, 완벽한 source of truth(진리의 원천)로 시작하는게 낫다는 것입니다. 이것은 유연하고, 심플하게 하며, 다른 상태들이 아니라 한 상태에 대해서만 업데이트 하여 공통의 data들에 대한 동기화하는 실수를 방지합니다.
- 상태들은 어디에 있어야 하는가? 상태관리는 여기서 다루기에는 광범위한 주제입니다. 그러나 일반적으로, 컴포넌트에 대해 로컬 상태로 만들 수 있다면, 그렇게 해야합니다. 컴포넌트가 전역 상태에 더 많이 의존될 수록, 재사용의 가능성이 줄어집니다. 이 질문은 어떤 컴포넌트가 어떤 상태에 의존해야하는지 식별하는데 유용합니다.
이 아티클에서 얻은 더 많은 지혜 :
컴포넌트는 오직 하나의 일만 하는 것이 이상적입니다. 컴포넌트가 성장하게 되면, 그것은 더 작은 서브 컴포넌트들로 분해되어야 합니다.
이 원칙들은 간단하고 실전 테스트를 거쳤으며, 복잡성을 길들이는데 효과적입니다. 컴포넌트를 만들 때, 가장 일반적인 정신 모델의 기초를 형성합니다.
간단하다는 것은 쉽다는 건 아닙니다. 실제로는 많은 팀과 개발자들과 함께 하는 큰 규모의 프로젝트에서 완료되는 것보다 쉽다는 맥락입니다.
성공적인 프로젝트들은 종종 기본 원칙을 일관되게 잘 지키는 것으로 부터 나옵니다. 그리고 많은 비용이 드는 실수를 만들지 않습니다.
이 심플한 원칙을 적용하지 못하는 상황은 무엇일까요?
우리는 이러한 상황을 최대한 완화하려면 어떻게 할 수 있을까요?
아래에서, 우리는 시간이 지남에 따라 왜 심플함을 유지하는 것이 실제로 그렇게 간단하지 않은 이유를 살펴보겠습니다.
4. top-down(상향식) vs bottom-up(하향식)
컴포넌트는 리액트와 같은 모던 프레임워크에서 추상화의 핵심 단위입니다. 컴포넌트를 만들는 것에 대해 2가지 주요 방식이 있습니다. 리액트로 사고하기에서 다음과 같이 말했습니다.
top-down 혹은 bottom-up 방식으로 만들 수 있습니다. 즉, 컴포넌트를 구축할 때 계층 구조에서 컴포넌트를 높이 쌓아가며 시작할 수 있다. 더 간단한 예로, top-down 방식이 종종 쉽지만, 대규모 프로젝트에서는 bottom-up 방식으로 하고, 테스트를 진행하는게 쉽다.
더 확실한 조언. 언뜻보면 간단해 보인다. “단일 책임은 좋다”를 읽는 것처럼 고개를 끄덕이고 넘어가기 쉽습니다.
그러나, top-down과 bottom-up 의 정신 모델 간의 차이는 표면적으로 보이는 것보다 더 중요합니다. 대규모로 적용될 때, 두 가지의 방식의 사고는 컴포넌트를 생성시 암묵적으로 광범위하게 공유될 때, 매우 다른 결과로 이어집니다.
5. top-down으로 구축하기
위의 인용문이 내포하고 있는 것은
간단한 예를 위해 top-down 접근 방식을 채택하여 쉽게 진행하는 것과,
대규모 프로젝트를 위해 느리지만 확장 가능한 bottom-up 접근 방식을 절충한 것이 포함되어 있습니다.
top-down 방식은 일반적으로 가장 직관적인 접근 방식입니다. 제 경험상, 컴포넌트를 구성하면서 개발자들이 기능 개발을 해야할 때 가장 일반적인 정신 모델입니다.
top-down 방식은 어떤 건가요? 디자인 작업이 주어졌을 때 가장 일반적인 조언은
“UI의 주변에 박스를 그리면, 그것은 컴포넌트가 될 것이다” 입니다.
이것은 우리가 만드는 최상위 컴포넌트의 기초를 형성합니다. 이 접근법과 함께, 우리는 처음부터 거친 컴포넌트를 생성하게 됩니다. 시작하기에 적합해 보이는 경계처럼 보이는 것과 함께.
우리는 새로운 어드민 대시보드가 만들 필요가 있어 디자인을 었었다고 가정해봅시다. 계속해서 디자인을 살펴보고, 무슨 컴포넌트를 만들어야 하는지 확인합니다.
디자인에 새로운 사이드바 네비게이션이 있습니다. 우리는 사이드바 주변에 박스를 만들고, <SideNavigation /> 이라고 부르는 새로운 컴포넌트를 만듭니다.
이 top-down 방식에 따라, 필요한 props가 무엇인지, 렌더링 방법에 때해 생각할 것입니다. 백엔드 API에서 네비게이션 아이템 리스트들을 얻는다고 가정해봅시다. 우리의 암시적인 top-down 모델에 따라, 아래의 pseudo code(의사 코드)과 같은 초기 디자인을 보는 것은 놀라운 일이 아닙니다.
// get list from API call somewhere up here // and then transform into a list we pass to our nav component const navItems = [ { label: 'Home', to: '/home' }, { label: 'Dashboards', to: '/dashboards' }, { label: 'Settings', to: '/settings' }, ] ... <SideNavigation items={navItems} />
top-down 방식은 상당히 단순하고 직관적으로 보입니다. 우리의 의도는 쉽고 재사용 가능하게 만드는 것이며, consummers는 그저 렌더링 되는 항목들에 들을 넘기고, SideNavigation은 그들을 통해 처리할 것입니다.
top-down 방식에서 공통적으로 유의해야할 사항들이 몇가지 있습니다.
- 처음에 우리가 필요한 컴포넌트로 식별하는 최상위 레벨의 경계에서 구축하는 것을 시작합니다. 우리가 디자인에서 그려야하는 박스부터 시작했습니다.
- 이것은 side navigation에서 관련된 모든 것을 처리하는 단일 추상화입니다.
- 이 API는 consummers가 상위 컴포넌트에서 작동이 가능한 data를 아래로 전달하고, 하위 컴포넌트에서 모든 것을 처리하는 의미에서 종종 top-down 방식인 경우가 많습니다.
우리의 컴포넌트가 종종 백엔드 data를 통해 직접 렌더링하는 경우가 많기 때문에, 렌더링할 컴포넌트 속으로 data를 아래로 전달하는 동일한 모델에 적합합니다.
작은 프로젝트에서는, 이 top-down 접근법이 특별히 잘못된 것은 아닙니다. 그러나 많은 개발자들이 빠르게 배포되어야 하는 큰 규모의 코드에서는, 우리는 top-down 정신 모델이 어떻게 빠르게 문제가 되는지 알아보겠습니다.
6. top-down 방식이 잘못된 경우
top-down 사고 방식은 당면한 문제를 해결하기 위해 게이트 밖의 특정 추상화에 고정되는 경향이 있습니다.
직관적입니다. 컴포넌트를 만드는 가장 직관적인 접근 방식처럼 느껴집니다. 또한 초기 사용의 용이성을 위해 최적화되는 API로 이어지는 경우가 많습니다.
다음은 다소 일반적인 시나리오입니다. 당신이 빠르게 개발해야 하는 프로젝트를 하고 있다고 합시다. 당신은 상자 레이아웃을 그리고, 스토리(여러 기능)를 만든 새 컴포넌트를 병합했습니다. 당신이 side navigation 컴포넌트에 업데이트가 필요한 새로운 요구사항도 따라왔습니다.
빠르게 일이 진행될 시기입니다. 큰 모노리틱 컴포넌트를 생성하게 되는 일반적인 상황입니다.
개발자는 변경 사항을 적용시키기 위해 스토리를 선택합니다. 개발자들은 이제 코딩할 준비가 되어있고, 이미 결정된 추상화 및 API의 맥락에 있습니다.
2가지 선택 사항이 있습니다.
A - 이것이 올바른 추상화인지 생각해보십시오. 그렇지 않다면, 스토리에 설명된 작업을 하기 전에, 적극적으로 분해하여 실행을 취소하세요.
B - 또 다른 prop을 추가하세요. 그리고 해당 속성을 확인하는 간단한 조건 뒤에 새로운 기능을 추가하세요. 새로운 prop을 통과하는 테스트를 작성하고, 작동되고 테스트가 통과될 것입니다. 보너스처럼 빠르게 끝날 것입니다.
Sandy Mets는 이렇게 말했습니다.
기존 코드는 강력한 영향력을 발휘합니다. 그 존재 자체는 그것이 정확하고, 필요하다고 주장합니다. 우리는 코드가 소비된 노력을 나타낸다는 것을 알고 있으며, 이러한 노력의 가치를 보존하려는 동기가 매우 강합니다. 그리고 불행하게도, 슬픈 사실은 코드가 복잡하고 이해하기 어려울수록, 즉 코드를 만드는데 더 많은 투자를 할수록, 코드를 유지해야한다는 압박감이 더 커집니다. (매몰 비용 오류)
매몰 비용 오류는 우리가 본능적으로 손실을 피하는데 더 민감하기 때문에 존재합니다. 당신이 마감시간이 촉박하거나, 스토리의 포인트가 +1 이 더해해지면서 시간 압박을 받을 때 당신(혹은 당신 동료)은 A를 선택하지 못할 확률이 높습니다.
규모면에서 이러한 작은 의사결정의 빠른 정점은 빠르게 누적되어 구성요소의 복잡성을 증가시키기 시작합니다.
불행히도 우리는 이제 “리액트로 사고하기”에서 설명된 근본적인 원칙 중에 하나를 실패하였습니다. 하기 쉬운 일은 종종 단순함으로 이어지지 않습니다. 그리고, 단순함으로 이끄는 것은 다른 대안들과 비교해 볼 때 쉽지 않습니다.
심플한 navigation sidebar 예를 들어 일반적인 시나리오에 적용해보겠습니다.
첫번째, 다자인의 변경 건이 들어옵니다. 그리고 우리는 nav item에 1) icon을 추가하고, 2) 다양한 크기의 text size, 3) 일부는 spa에서 page 전환이 아니라 링크로 연결되도록 하는 요구사항을 추가하고 싶습니다.
실제로 UI는 많은 시각적 상태를 유지합니다. 우리는 또한, 구분 기호, 새탭에서 열기, 기본 상태를 선택한 일부 등을 원합니다.
우리는 side bar 컴포넌트에 nav items에 대해 배열로 전달하기 때문에, 새로운 요구사항들에 대한 새 유형과 다양한 상태를 구별하기 위해서 nav items의 object에 추가적인 속성들을 넣을 필요가 있습니다.
따라서 현재 우리는 링크인지, 일반적인 nav item인지 해당하는 type의 item이 {id, to, label, icon, size, type, separator, isSelected} 등으로 보일 수 있습니다.
그리고 <SideNavigation/> 컴포넌트 안에서, type을 체크하고 nav item에 대해 적절히 렌더링 할 것입니다. 이런 작은 변화에 벌써부터 조금씩 (안좋은) 냄새가 나기 시작합니다.
여기에서 문제는 이런 API가 있는 top down 방식의 컴포넌트들은 api에 추가함으로써 넘겨온 값을 기반으로 내부적으로 로직을 분기하여 요구사항에 대응해야 합니다.
작은 것으로부터 큰 것이 된다.
몇주 후 새로운 기능은 요청되었는데, nav item을 클릭하고, 중첩된 sub 그 nav item 아래에 navigation을 탐색할 수 있어야 하며, 뒤로가기 버튼을 사용하여 메인 navigation list에 돌아갈 수 있어야 한다. 우리는 또한 관리자가 drag and drop 기능을 통해 navigation items 을 재정렬 할 수 있게 해야 합니다.
이제 목록을 중첩하고, 서브 리스트를 부모인 nav item에 연결하고, 몇가지 items을 drag할 수 있게 해야합니다.
몇가지 요구사항은 변경되고, 당신은 점점 어떻게 복잡해지기 시작는지 볼 수 있습니다.
간단한 API를 사용하는 비교적 간단한 컴포넌트로 시작한 것은 몇번의 반복과 함께 빠르게 커집니다. 우리의 개발자들은 제 시간에 작업을 수행한다고 가정해보겠습니다.
이 시점에서 이 컴포넌트를 사용하거나 변경해야하는 다음 개발자 혹은 팀은 복잡한 구성이 필요한 가장 모노리틱 컴포넌트를 다루고 있습니다.
“목록을 전달하고 컴포넌트는 나머지를 처리한다” 라는 우리의 초기 의도는 이 시점에서 역효과가 났고, 컴포넌트는 변경하기에 느리고 위험합니다.
이 시점에서 일반적인 시나리오는 모든 것을 버리고 처음부터 컴포넌트를 다시 만드는 것입니다. 이제 첫번째 반복에서 해결해야 할 문제와 사용사례를 이해했습니다.
7. 모놀리식 컴포넌트의 유기적 성장
처음을 제외하고, 모든 것은 top-down으로 만들어야 합니다.
우리가 앞에서 살펴본 것 처럼, 모노리틱 컴포넌트는 너무 많은 것을 하는 컴포넌트입니다. 그것들은 너무 많은 data 또는 props를 통한 구성 옵션을 취급하고, 너무 많은 state를 관리하고, 너무 많은 UI를 출력합니다.
그것들은 종종 단순한 컴포넌트에서 시작하여, 위에서 설명한 것 처럼 복잡성의 유기적인(유기적으로 연결된) 성장을 통해 너무 많은 작업을 수행하게 됩니다.
단순한 컴포넌트로 시작한 것은 몇번의 반복 내에(심지어 동일한 스프린트 내에서도 조차) 당신이 만든 새 기능들이 모노리식 컴포넌트가 될 수 있습니다.
빠르게 개발해야 하는 상황 속에서 동일한 코드베이스 상에 여러 컴포넌트들이 이러한 일이 발생되면, 프론트엔드는 빠르게 변화하기 어려워지고 사용자에게는 더 느려집니다.
다음은 모놀리식 컴포넌트들이 소리없이 파멸로 이어질 수 있는 몇가지 방법입니다.
- 조기 추상화를 통해 발생
모노리식 컴포넌트로 이끄는 또다른 미묘한 문제가 있습니다. 소프트웨어 개발자로서 초기에 주입되는 몇가지 일반적인 모델과 관련이 있습니다. 특히 DRY(=Don’t repeat yourself) 준수하기.
DRY가 초기에 내제되어있다는 사실과 컴포넌트가 구성된 사이트에서 소량의 중복을 볼 수 있습니다. “많이 중복되고 있으니, 이것은 단일 컴포넌트로 추상화하기 좋은 것”이라고 생각하기 쉽고, 우리는 초기 추상화에 돌입합니다.
모든 것의 절충점이지만, 잘못된 추상화보다 추상화가 없는 상태에서 복구하는 것이 훨씬 쉽습니다. 그리고 우리는 아래에서 더 이야기하겠지만, bottom-up 방식의 모델로 시작하면 이러한 유기적으로 추상화에 도달 할 수 있고, 조기에 추상화를 생성하는 것을 피할 수 있습니다.
- 팀 전체에서 코드 재사용을 막는다.
당신은 당신의 팀에서 원하는 비슷한 것을 종종 다른 팀이 구현했거나, 작업 중인 것을 종종 발견하게 될 것입니다.
대부분의 경우 원하는 기능의 90%는 수행하지만, 약간 변형이 필요합니다. 또는 전체 기능을 사용하지 않고 특정 부분의 기능만 재사용하고 싶을 수 있습니다.
<SideNavigation/> 과 같은 전부 아니면 전무의 모놀리식 컴포넌트라면, 기존의 작업을 활용하는 것은 어려울 것입니다. 다른 사람의 패키지를 리팩토링하거나 분해하는 위험을 감수하는 대신. 종종 다시 구현하고 안전하게 자신의 패키지로 확보하는 것이 더 쉬운 경우가 많습니다. 약간의 변형이 있고 동일한 문제가 있는 여러 중복된 컴포넌트들로 이어집니다.
- 번들 사이즈 증가
적시에 로드, 파싱, 실행할 필요가 있는 코드만 허용하려면 어떻게 해야할까요?
사용자에게 먼저 보이는게 더 중요한 몇가지 컴포넌트들이 있습니다. 대규모의 어플리케이션을 위한 핵심 성능 전략은 우선 순위에 따라 “phases”에서 로드된 코드를 비동기로 조정하는 것입니다.
컴포넌트에 서버에서 렌더링 되는 기능을 주는 것 외에도 여기서는 가능한 연기하는 아이디어가 있습니다.
모놀리식 컴포넌트는 하나의 큰 청키 컴포넌트로서 모든 것을 로드해야하기 때문에 그 노력을 막습니다. 최적화할 수 있고, 유저가 진짜로 필요할 때만 로드할 수 있는 독립적인 컴포넌트를 갖는 것이 아닙니다. 소비자는 그것을 사용하는 성능 비용을 지불하는 경우에.
- 런타임 성능 저하
상태 → UI 의 단순한 기능 모델을 가진 리액트와 같은 프레임워크는 매우 생산적입니다. 그러나, 가상 DOM에서 변화되는 것을 확인하기 위한 조정 프로세스는 규모면에서 비용이 많이 듭니다. 모놀리식 컴포넌트들은 상태가 변경될 때 최소한의 항목만 리렌더링되도록 하기 매우 어렵습니다.
가상 DOM으로서 리액트와 같은 프레임워크에서 렌더링 성능을 높이는 가장 단순한 방식은 변경되는 컴포넌트들을 나누는 것입니다.
따라서 상태가 변경되면, 오직 필요한 것만 리렌더링 됩니다. 선언적 데이터를 가져오는 Relay와 같은 프레임 워크를 사용한다면, 데이터가 업데이트가 발생할 때 서브 트리에서 비용이 비싼 리렌더링을 방지하기 위해 이 기술은 점점 더 중요해집니다.
모노리틱 컴포넌트와 top-down 접근 방식 내에서는 일반적으로 분할을 찾는 것은 어렵고 에러가 발생하기 쉬우며, 종종 use memo를 자주 사용하게 됩니다.
8. bottom-up 으로 구축하기
top-down 방식과 비교해서, bottom-up 방식은 종종 덜 직관적이고 초기에 느릴 수 있습니다. 큰 부억 싱크대 같은 스타일의 컴포넌트 대신 API를 재사용할 수 있는 여러개의 작은 컴포넌트를 만들게 됩니다.
빠르게 출시하려고 할때, 이 방식은 모든 컴포넌트가 실제로 재사용할 필요가 없기 때문에 직관적이지 않은 접근 방법입니다.
하지만 실제로 재사용하지 않아도 API를 재사용할 수 있는 컴포넌트로 만드는 것은 일반적으로 더 가독성이 좋고, 테스트하기 쉬우며, 변경과 삭제에 용이한 컴포넌트 구조가 됩니다.
컴포넌트를 어디까지 분해해야 하는지는 정답은 없습니다. 이를 관리하는 핵심은 단일 책임 원칙을 사용하는 것입니다.
bottom-up 모델과 top-down 모델은 많이 다른가요?우리는 예로 돌아가보겠습니다. bottom-up 방식은 여전히 상위 레벨에 <SideNavigation/>을 만들 것입니다. 하지만 이를 구축하는 방식은 모든 차이를 만들어 냅니다.
우리는 상위 레벨에서 <SideNavigation /> 컴포넌트를 구별하지만, 여기서 작업이 시작되지 않는다는 점이 차이입니다.
<SideNavigation />의 기능을 구성하는 모든 기본 요소들을 분류하고, 함께 구성될 수 있는 작은 부분들로 구성하는 것으로 시작합니다. 이런 방식으로, 시작할 때는 약간 덜 직관적입니다.
전체 복잡성은 하나의 모놀리식 컴포넌트에 비해 많은 작은 단일 책임성을 갖춘 컴포넌트로 분산이 됩니다.
bottom-up 방식이 어떻게 보이시나요?
side navigation의 예로 돌아가보겠습니다. 다음은 간단한 경우의 예입니다.<SideNavigation> <NavItem to="/home">Home</NavItem> <NavItem to="/settings">Settings</NavItem> </SideNavigation>
단순한 경우에는 주목할 것은 없습니다.
그럼 중첩 그룹을 지원해야 하는 API는 어떨까요?
<SideNavigation> <Section> <NavItem to="/home">Home</NavItem> <NavItem to="/projects">Projects</NavItem> <Separator /> <NavItem to="/settings">Settings</NavItem> <LinkItem to="/foo">Foo</NavItem> </Section> <NestedGroup> <NestedSection title="My projects"> <NavItem to="/project-1">Project 1</NavItem> <NavItem to="/project-2">Project 2</NavItem> <NavItem to="/project-3">Project 3</NavItem> <LinkItem to="/foo.com">See documentation</LinkItem> </NestedSection> </NestedGroup> </SideNavigation>
bottom-up 방식의 최종 결과는 직관적입니다. 더 간단한 API의 복잡성이 개별 컴포넌트 뒤에 캡슐화 되므로 더 많은 노력이 필요합니다. 그러나, 이것은 더 소비가능하고, 적응 가능한 장기적인 접근 방식을 만드는 것입니다.
top-down 방식과 비교해서 얻는 많은 이점:
1) 컴포넌트를 사용하는 여러 팀들은 그들이 실제로 import하고 사용하는 컴포넌트에 대해서만 비용을 지불하면 됩니다.
2) code split와 유저를 위한 우선순위가 높지 않은 element를 비동기적으로 로드하는게 쉬워집니다.
3) 업데이트로 인해 변경되는 하위 트리만 다시 렌더링됨으로, 렌더링 성능은 더 좋고, 관리하기 쉬워집니다.
4) nav안에서 특정 책임이 있는 개별 구성 요소로 만들고 최적화할 수 있습니다. 또한, 각 컴포넌트를 독립적으로 작업하고 최적화 할 수 있기 때문에 코드 구조 관점에서 더 확장 가능합니다.주목해야 할 사항은 무엇일까요?
bottom-up 방식은 처음에 느리지만, 적응력이 높기 때문에 장기적인 관점에서 빠릅니다. 성급한 추상화를 쉽게 피할 수 있고, 올바른 추상화가 분명해질 때가지 시간이 지남에 따라 변화의 물결을 탈 수 있습니다. 모놀리식 컴포넌트의 확산을 방지하기 위한 최고의 방법입니다.
sidebar nav와 같은 코드 베이스 전체에서 사용되는 공통 컴포넌트인 경우, bottom-up 방식으로 구축하는 것은 소비자 측면에서 요소들을 조립하는데 꽤 노력이 필요합니다.
그러나 우리가 본 것처럼 많은 공유 컴포넌트들이 있는 큰 규모에서 가치를 만드는 절충안입니다.
bottom-up 접근 방식의 장점은 이미 특정 추상화를 염두에 두고 시작하는 것과 비교하여 “내가 원하는 것을 달성하기 위해 함께 구성할 수 있는 간단한 프리미티브는 무엇인가” 라는 전제로 시작하는 것입니다.
”에자일 소프트웨어 개발의 가장 중요한 교훈 중 하나는 반복의 가치입니다. 이는 아키텍쳐를 포함하여 소프트웨어 개발의 모든 레벨에 적용됩니다”
상향식 접근 방식을 사용하면 장기적으로 더 잘 반복할 수 있습니다.
다음으로, 만드는 것을 더 쉽게 만드는 유용한 원칙을 요약해보겠습니다.
9. 모놀리식 컴포넌트를 피하는 전략
1. 단일 책임 vs DRY의 균형
bottom-up 사고는 종종 합성 패턴을 수용하는 것을 의미합니다. 이는 꽤 소비 시점에서 약간의 중복이 있음을 의미합니다.
DRY는 개발자로서 우리가 처음 배우는 것이고, 코드를 DRY하는 것은 기분이 좋습니다. 그러나 모든 것을 DRY하기 전에 필요한지 확인하고 기다리는 것이 더 좋습니다.
그러나 이 접근 방식을 사용하면, 프로젝트가 성장하고 요구사항이 변경될때마다 복잡성의 물결을 타게 할 수 있으며, 합리적인 시점에서 쉬운 소비를 이끄는 추상화를 허용합니다.
2. 제어의 역전
이 원칙을 이해하기 위한 간단한 예는 콜백과 프로미스의 차이입니다.
콜백을 사용하면 해당 함수가 어디로가는지, 몇번이나 호출되는지, 또는 무엇으로 호출되는지 알 필요가 없습니다.
promise는 제어권을 소비자에게 되돌려 논리를 구성하기 시작하고 값이 이미 준비된 것처럼 가정할 수 있습니다.
// may not know what onLoaded will do with the callback we pass it onLoaded((stuff) => { doSomething(stuff) }) // control stays with us to start composing logic as if the // value was already there onLoaded.then((stuff) => { doSomething(stuff) })
리액트 맥락에서, 우리는 이것이 component API 디자인을 통해 달성되는 것을 볼 수 있습니다.
자식 컴포넌트를 통해 slot을 노출시키거나, 소비자 측면에서 제어의 역전을 제공하는 props style로 렌더링 할 수 있습니다.
때때로 소비자는 더 많은 일을 해야하는 것 같은 느낌이 있기 때문에, 이와 관련하여 제어의 역전에 대해 혐오감이 있습니다. 그러나 미래를 예측할 수 있다는 생각을 포기하고 소비자에게 유연성을 부여하는 것을 선택하는 것입니다.
// A "top down" approach to a simple button API <Button isLoading={loading} /> // with inversion of control // provide a slot consumers can utilize how they see fit <Button before={loading ? <LoadingSpinner /> : null} />
두번째 예는 <LoadingSpinner />가 더 이상 Button 컴포넌트에 내부에 종속될 필요가 없기 대문에 요구사항에 더 유연하고 성능이 더 뛰어납니다.
여기에서 bottom-up과 top-down에 대해 미묘한 차이를 볼 수 있습니다. 첫번째 예에서는 데이터를 전달하고 컴포넌트가 이를 처리하도록 합니다. 두번째 예에서는 조금 더 많은 작업을 수행해야하지만, 궁극적으로 더 유연하고 성능이 뛰어난 접근 방식입니다.
<Button /> 자체가 후드 아래에서 더 작은 기본 요소로 구성될 수 있다는 점도 흥미롭습니다. 때때로 특정 추상화는 명시적으로 만들 수 있는 다양한 하위 동작 요소들을 가지고 있습니다.
예를 들어 LinkButton 과 같은 컴포넌트를 생성하기 위해, 결합할 수 있는 Link 컴포넌트와 button 컴포넌트가 모두 적용 되는 Pressable과 같은 컴포넌트로 세분화 할 수 있습니다. 이보다 더 세분화된 분류는 종종 디자인 시스템 라이브러리의 영역에 남습니다만, 제품 중심의 엔지니어로서 염두에 둘 가치가 있습니다.
3. 확장을 위한 오픈
합성 패턴을 사용하여 bottom-up 방식으로 구축하는 경우도 마찬가지입니다. 여전히 소비가능한 API를 사용하여, 작은 컴포넌트로 만들어진 특수한 컴포넌트를 export하기를 원합니다. 유연성을 위해 패키지에서 해당하는 특수한 컴포넌트를 구성하는 더 작은 블록 단위를 노출할 수 있습니다.
이상적으로 컴포넌트는 한가지 일을 합니다. 그래서 미리 만들어진 추상화의 경우, 소비자는 필요한 하나의 컴포넌트를 가져와서, 그들의 자체 기능으로 확장시켜 포장할 수 있습니다. 또는, 기존 추상화를 구성하는 몇 가지 컴포넌트들을 선택하고, 원하는 것을 구축할 수 있습니다.
4. 스토리북 기반의 개발 활용
일반적으로 컴포넌트에서 관리되는 수많은 개별 상태가 있습니다. 상태 머신 라이브러리는 합당한 이유로 점점 인기를 얻고 있습니다. 우리는 스토리북과 별도로 UI 컴포넌트를 구성할 때 사고 뒤의 숨겨진 모델을 채택하고 컴포넌트가 있을 수 있는 각 유형의 가능한 상태에 대한 스토리를 가질 수 있습니다. 외와 같이 사전에 수행하면 운영 환경에서 좋은 오류 상태를 구현하는 것을 잊어버렸다는 것을 피할 수 있습니다. 또한, 작업 중인 컴포넌트를 구축할 때 필요한 모든 하위 구성요소를 식별하는데 도움이 됩니다.
탄력적인 컴포넌트로 이어지는 UI 컴포넌트를 독립적으로 구축할 때 스스로에게 물어볼 수 있는 질문들
1. 접근 가능한가?
2. 로딩 중일 때 어떻게 보이는가?
3. 어떤 데이터에 의존하는가?
4. 어떻게 에러를 처리하는가?
5. 데이터의 일부만 사용할 수 있는 경우 어떻게 되는가?
6. 이 컴포넌트가 여러번 마운트되면 어떻게 되는가? 즉 어떤 부작용이 있는지, 만약 이것이 내부 상태를 관리한다면 상태가 일관적일 수 있을까?
7. 불가능한 상태와 이러한 상태간의 전환을 어떻게 처리하는가? 예를 들어 로딩과 에러 props가 둘다 true라면 어떤 일이 일어나는가? (이 예에서는 아마 컴포넌트 API를 다시 재고할 수 있는 기회일 것이다)
8. 그것은 얼마나 조합할 수 있는가? API에 대해 생각해보자.
9. 여기에 다양한 즐거운 효과를 볼 수 있나요? 예를 들어 미묘한 애니메이션이 잘 동작되는지다음은 탄력적인 컴포넌트 구축을 하기 위한 일반적인 방법입니다.
- 실제 하는 작업을 기반으로 컴포넌트 이름 짓기 단일 책임 원칙으로 다시 돌아가봅시다. 이름이 합당하다면 긴 이름도 두려워하지마세요.
또한 실제로 하는 작업보다 더 일반적인 컴포넌트 이름으로 지정하는게 쉽습니다. 실제 하는 작업보다 더 일반적으로 이름이 지정되면, 다른 개발자들에게 그것은 x와 관련된 모든 것을 처리하는 추상화하는 것이라고 알려줍니다.
따라서 새로운 요구사항이 있을 때 변경해야하는 분명한 구간으로 표시가 됩니다. 그렇게 하는 것이 말이 안되는 경우에도 마찬가지입니다.
- 구현 세부 정보를 포함하는 props 이름을 피하기 특히 UI 스타일 ‘leaf’ 컴포넌트의 경우 더욱 그렇습니다. 내부 상태 혹은 특정 도메인과 관련된 isSomething과 같은 props를 추가하지 않는게 좋습니다. 그런 다음 prop이 전달될 때 컴포넌트는 다른 작업을 수행하도록 합니다.
만약 이를 수행해야 하는 경우, prop 이름이 그것을 소비하는 컴포넌트 문맥에서 실제로 하는 것을 반영하는지가 더 명확합니다.예를 들어, isSomething prop은 padding과 같은 것을 제어하게 되면, prop 이름은 컴포넌트가 겉보기에는 관련이 없어 보이는 것을 인식하지 않고 대신 이를 반영해야합니다.
- props에 의한 구성에 주의하세요. 제어의 역전으로 돌아가봅시다. <SideNavigation navItems={items} /> 과 같은 컴포넌트는 한가지 유형의 자식 컴포넌트(그리고 이게 절대 바뀌지않는다고 확정이 된다면)만 가진다면 괜찮게 작업이 될 수 있다.
그러나 우리가 보았듯이 빠른 출시를 시도하는 여러 팀과 개발자 간에 확장하기에 어려운 패턴입니다. 그리고 실제로는 변화에 대한 탄력성을 잃게 하고 복잡성을 빠르게 증가시키는 경향이 있습니다.
우리는 종종 컴포넌트의 자식이 다르거나 추가적인 타입을 갖게 확장하려고 합니다. 즉, 해당 구성의 옵션, props에 더 많은 것들을 추가하고 분기 로직을 만듭니다.
소비자가 객체를 정렬하고 전달하도록 하는 대신, 더 유연한 접근은 내부의 자식 컴포넌트를 export하여, 소비자가 컴포넌트를 구성하고 전달하도록 하는 것입니다.
- render 메서드에 컴포넌트를 정의하지 마세요. 때때로 컴포넌트 안에 helper 컴포넌트를 가질 수 있습니다. 이것은 결국 렌더링할 때마다 다시 리마운트가 될 것이고, 이상한 버그를 가져올 것입니다.
또한 여러개의 내부 renderX, renderY를 갖는 것은 이상한 냄새가 날 수 있습니다. 이는 일반적으로 컴포넌트가 모놀리틱한 컴포넌트로 되고 있다는 사인이며, 분해를 해야 하는 좋은 후보입니다.
10. 모놀리식 컴포넌트의 분해
가능한 자주 그리고 일찍 리팩토링을 하세요. 변경이 될 가능성이 높은 구성요소를 식별하고, 그것들을 적극적으로 분해하는 것은 좋은 전략입니다.
프론트엔드가 지나치게 복잡하게 된 경우 어떻게 하시겠습니까?
여기에 2가지 옵션이 있습니다.
1. 다시 코드를 작성하고, 새로운 컴포넌트로 점진적으로 마이그레이션하기
2. 점진적으로 컴포넌트를 분해하기
컴포넌트 리팩토링 전략은 이 가이드와 범위가 벗어납니다. 그러나 활용할 수 있는 기존의 실전 테스트 리팩토링 패턴이 많이 있습니다.
리액트와 같은 프레임워크에서, 컴포넌트는 실제로 위장된 함수입니다. 기존의 모든 검증된 리팩토링 기술에서 함수라는 단어를 컴포넌트로 대체할 수 있습니다.
몇가지 관련 예시:
- Remove Flag Argument
- Replace Conditional with Polymorphism
- Pull Up Field
- Rename Variable
- Inline Function
11. 결론
우리는 여기서 많은 근거를 다루었습니다. 이제 주요 내용에 대해 요약해보겠습니다.
- 우리가 가지고 있는 모델은 프론트엔드 컴포넌트를 설계하고 구축할 때, 많은 미세한 결정에 영향을 줍니다.
이것들을 명시적으로 만드는 것은 꽤 빠르게 축적되기 때문에 유용합니다. 이러한 결정의 축적은 궁극적으로 가능한 것이 무엇인지 결정합니다. 즉, 새로운 기능을 추가하기 위해 마찰을 늘리거나 줄이거나 더 확장할 수 있는 새로운 아키텍처를 채택하는 것입니다. - 컴포넌트를 구성할 때, top-down과 bottom-up 진행 방식의 차이는 규모에 따라 결과가 크게 다를 수 있습니다.
top-down 방식은 컴포넌트 구축시, 일반적으로 가장 직관적입니다. UI를 분해할 때 가장 일반적인 모델은 기능 영역 주변에 박스를 그리는 것입니다. 이 기능 분해 프로세스는 top-down이며, 종종 특정 추상화를 사용하여 특별한 컴포넌트를 생성합니다. 요구사항이 변경되고, 이러한 몇번의 반복을 통해 이 컴포넌트는 빠르게 모놀리식 컴포넌트가 됩니다. - top-down 방식으로 설계하고 구축하면 모놀리식 컴포넌트가 될 수 있습니다.
모놀리식 컴포넌트로 가득 찬 코드베이스는 느리고 변화에 탄력적이지 않은 프론트엔드 아키텍쳐를 초래합니다. 모놀리식 컴포넌트는 다음과 같은 이유로 안좋습니다.
- 변경 및 유지관리 비용이 많이 듭니다.
- 변경 시 위험합니다.
- 팀 간의 기존 작업을 활용하는 것이 어렵습니다.
- 안좋은 성능을 가지고 있습니다.
- 효과적인 코드 분할, 팀 간 코드 재사용, 로딩 단계, 렌더링 성능 등과 같은 프론트엔드를 지속적으로 확장하는데 중요한 미래 지향적 기술 및 아키텍쳐를 채택할 때 마찰을 증가시킵니다.
- 조기 추상화의 생성 혹은 지속적인 확장으로 이어지는 기본 모델과 상황을 이해함으로써 모놀리식 컴포넌트 생성을 피할 수 있습니다. 리액트는 컴포넌트를 설계할 때 bottom-up 방식이 효과적입니다. 이를 통해 조기 추상화를 효과적으로 방지할 수 있습니다. 우리는 “복잡성의 물결을 타고” 적절한 시기에 추상화 할 수 있습니다. 이러한 방식으로 구축하면 컴포넌트 합성 패턴을 실현할 가능성이 높습니다. 모놀리식 컴포넌트가 실제로 얼마나 많은 비용이 많이 드는지 알고 있으므로 표준 리팩토링 방식을 적용하여 일상적으로 제품 개발의 일부를 정기적으로 분류할 수 있습니다.
마치며
빠르게 개발을 해야할 때, 그동안 top-down 방식으로 컴포넌트를 작성해왔다.
하지만, 작성할 때마다 추상화하거나 props를 추가할 때마다 코드에 대해 의심이 가기도 하였는데,
이러한 방식이 점차 개발하면서 더 변경하기 힘든 구조로 이끄는 것을 깨달았다.
번역 과정을 거치면서 bottom-up 방식을 잘 이해할 수 있었으며,
최대한 bottom-up 방식으로 코드를 작성하도록 노력해야겠다.
'Frontend' 카테고리의 다른 글
[Frontend] SEO 최적화를 위한 Meta tag (0) 2023.03.15 [frontend] safari 브라우저에서, autocomplete ="off" not working bug (0) 2022.12.04 [I18N] i18n이란? (0) 2022.11.13 [TDD] test code 작성하기 ( with jest, react testing library) (0) 2022.08.15 [TDD] test code 관련 eslint 설정 (0) 2022.07.26