들어가며
- 프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법 (이문기)를 읽고 공부한 내용을 요약해 보았습니다.
글을 읽는 이유
FE와 BE 협업 프로젝트로 야놀자 클론 프로젝트를 한 경험이 있습니다.
이때 가장 어려움을 겪었던 점은 공통 컴포넌트를 사용하는 페이지가 늘어나면서 공통 컴포넌트에 점점 많은 조건이 추가 되었고, 이로인해 더 이상 공통 컴포넌트라고 부를 수 없을 만큼 코드 복잡도가 높아졌던 점입니다.
이에 프로젝트 종료 후 유지 보수에 어려움을 겪고있어 해당 글을 통해 컴포넌트를 분리하는 기준과 방법을 알아보고 싶습니다.
요약
컴포넌트는 웹 앱을 구성 하는 데 있어 가장 작은 단위이다.
컴포넌트를 나누는 기준
A. 재사용성 컴포넌트
- HTML 요소를 고려한 컴포넌트 분리
- 중복을 고려한 컴포넌트 분리 (props, children 활용하기)
B. 복잡성 컴포넌트
- 컴포넌트가 여러 책임을 가질 때 분리
- 컴포넌트에 비지니스 로직이 있을 때 분리
C. 리렌더링 컴포넌트
- 컴포넌트와 관련이 없는 상태가 있을 때 분리
주요 내용
컴포넌트에 대한 전반적인 내용
컴포넌트
- 웹 앱을 구성 하는 데 있어 가장 작은 단위
컴포넌트를 만들 때 가장 많이 발생하는 실수 다섯 가지
- 복잡한 컴포넌트를 만든다.
- 하나의 컴포넌트에 여러 책임을 추가한다.
- 몇몇 동작하는 부분을 결합하여 컴포넌트를 만든다.
- 비지니스 로직을 컴포넌트에 추가한다.
언제 나눠야 할까?
컴포넌트를 만드는 기준, 즉 나누는 기준 중 가장 많이 선택되는 이유는 (A) 재사용성과 (B) 복잡성입니다.
컴포넌트를 나누는 기준
A. 재사용성 컴포넌트
HTML 요소를 고려한 컴포넌트 분리
function ListComponent(...) {
return (
<ul>
<li>
<h3>...</h3>
<p>...</p>
</li>
<li>
<h3>...</h3>
<p>...</p>
</li>
</ul>
);
}위 코드의 컴포넌트를 분리한다고 했을때,
li
태그로 묶은 ItemComponent 보다, 리스트 요소 말고 다른 곳에서도 사용할 수 있는 SomethingComponent를 하나의 컴포넌트로 묶는 것이 좋다.function ItemComponent(...) {
return (
<li>
<h3>...</h3>
<p>...</p>
</li>
);
}
// 보다
function SomethingComponent(...) {
return (
<>
<h3>...</h3>
<p>...</p>
</>
);
}중복을 고려한 컴포넌트 분리 (props, children 활용하기) 이렇게 둘 이상의 컴포넌트에서 사용할 재사용 가능한 컴포넌트를 만들 때 가장 큰 특징 중 하나는 조건문 입니다.
완벽하게 같은 걸 사용하면 문제가 안 되지만 서로 다른 부분이 있다면 조건문이 들어가게 됩니다. 우리가 현실에서 마주하는 재사용 컴포넌트는 아래와 같이 점점 거대해지곤 합니다.
function Page1() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}
function Page2() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}
function Card(props) {
const [a, setA] = useState(props.a ? props.foo : props.bar);
const condition1 = props.a && !props.b;
return (
<section>
<h3>...</h3>
<p>가격...</p>
<div>
<button>{a ? 'fooValue' : 'barBalue'}</button>
</div>
{props.showSummary && <p>요약...</p>}
{condition1 && <div>...</div>}
</section>
);
}Page1 컴포넌트와 Page2 컴포넌트는 Card 컴포넌트에 의존적입니다.
만약 Page1과 Page2 뿐만 아니라 더 많은 컴포넌트가 Card 컴포넌트를 사용할수록 문제는 더 심각해집니다.
이런 문제는 왜 발생했을까요? 그리고 어떤 문제를 일으킬까요?
가장 먼저 컴포넌트가 반환하는 요소의 중복을 추출해서 재사용해야 한다는 접근 방법이 문제의 발단일 수 있습니다.
추출한 컴포넌트 내부에 사용하는 방법에 따라 조건문이 추가된다는 건, 사용하는 컴포넌트들이 서로 다른 수정의 이유를 갖는 다는 걸 의미합니다. 즉, 중복 제거와 재사용의 대상이 아닙니다. 따라서 처음에 조건문이 들어갈 때부터 산불의 작은 불씨가 시작된 것이었습니다.
이 문제들을 해결하는 방법 중 하나는 재사용하려는 컴포넌트에는 정말 공통적인 것들만 남겨두고 사용하는 컴포넌트의 고유한 것은 속성(props)으로 전달하는 것입니다.
function Page1() {
return (
<ul>
<li>
<Card
summary={<p>요약...</p>}
/>
</li>
</ul>
);
}
function Page2() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}
function Card(props) {
return (
<section>
<h3>...</h3>
<p>가격...</p>
{props.summary}
</section>
);
}이렇게 상태나 조건문 등의 결합이 사라진 것만으로도 Page1만의 특징인 summay는 Page1이 관리하고 Card는 이에 대해 신경 쓸 필요가 없습니다.
특히나 이 방법은 props drilling을 피하거나 컴포넌트의 제어를 역전하는 등 좋은 점을 더 많이 갖고 있고 공식문서에서도 소개하는 만큼 반드시 숙지하고 있을 필요가 있습니다.
B. 복잡성 컴포넌트
컴포넌트가 여러 책임을 가질 때 분리
function Page(props) {
// 선택한 탭을 변경하면 보여주는 내용을 변경합니다.
// 페이징을 다룹니다.
// 단어를 검색을 합니다.
// 검색 조건 토글을 다룹니다.
// 등등
}이렇게 되면 기능 간에 결합이 강하게 발생해서 수정이 쉽지않습니다.
그렇기 때문에 컴포넌트를 책임에 맞게 나눠서 단순화 해야 합니다.
Page 컴포넌트가 탭, 검색, 페이징 그리고 이 정보들을 취합해 컨텐츠를 보여주는 등 모든 책임을 갖지 않도록 해야 합니다.
컴포넌트에 비지니스 로직이 있을 때 분리
일반적으로 유저 인터페이스(UI)와 비지니스 로직은 변경의 속도, 즉 빈도가 다릅니다. 이때 컴포넌트에 비지니스 로직이 포함되어있다면 빈번한 UI 변경에 따라 자주 영향을 받을 수 있습니다.
따라서 UI와 비지니스 로직을 적절하게 분리하는 건 소프트웨어를 오랫동안 유지보수 하는 데 있어서 아주 중요합니다.
C. 리렌더링 컴포넌트
컴포넌트와 관련이 없는 상태가 있을 때 분리
하나의 컴포넌트 안에서 서로 영향을 주지 않는 상태가 여럿 있으면 불필요한 렌더링이 발생하는 문제입니다.
function Page1() {
const [카드 호버 상태, set카드 호버 상태] = useState(false);
const [탭 호버 상태, set탭 호버 상태] = useState('none');
return (
...
<ul>탭</ul>
...
<ul>카드</ul>
...
);
}이 코드에서 탭과 카드는 서로 영향을 주지 않습니다. 하지만 탭에 호버를 하면 카드들이 렌더링되고 카드에 호버를 하면 탭이 렌더링 됩니다.
function Page1() {
return (
...
<Tab>
...
<ul>
...
<li><Card><li>
...
</ul>
...
);
}
function Tab() {
const [탭 호버 상태, set탭 호버 상태] = useState('none');
return (
<ul>탭</ul>
);
}
function Card() {
const [카드 호버 상태, set카드 호버 상태] = useState(false);
return (
<section>...</section>
);
}