도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

컨텐츠 vs 컨테이너 (Content vs Container)

어렸을 때 위의 게임을 많이 해봤을 것이다. 저 게임을 하면서 큐브에 다른 종류의 모양을 맞추는 법을 배웠을 것이다. 이 게임은 다음 이야기에 대한 좋은 은유가 될 것이다.

예제를 통해 컴포넌트 간의 책임을 혼합하는 방법과 단일 책임 원칙을 적용하는 방법을 파악하는 방법을 살펴보도록 하겠다. 우리는 컨텐츠와 컨테이너가 무엇인지에 대해서 자세하게 알아볼 것이다.

잘못된 패턴인 혼합 책임 패턴에 빠지면 소스 코드의 많은 부분에서 컴포넌트 스타일 뿐만 아니라 컴포넌트 로직에도 영향을 미칠 수 있다. 단일 책임 원칙의 주요 목적은 재사용 성 측면에서 더 나은 컴포넌트를 얻는 것이다. 이 예제를 사용하여 다음과 같이 분석 할 것이다:

  • 컴포넌트 스타일링(UI)
  • 컴포넌트 비지니스 로직 (UX)

*이 글에 제시된 모든 예제는 리액트 클래스와 JSX 마크 업을 기반으로하지만 이러한 기술은 사용하는 기술에 상관없이 적용된다.


장바구니 목록

장바구니 목록을 상상 해보자. 장바구니에 3 개의 아이템이 있으며 그 중 2 개가 선택되어있고, 그 중 하나는 프리미엄 아이템이다. 각 아이템에는 사용자가 장바구니에서 아이템을 제거 할 수있는 버튼이 있다.

쉬운 예를 들면 다음과 같다:


<ul class="cart">
<li>
<header>Item 1</header>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing.</p>
<button>Remove from cart</button>
</li>
<li class="premium selected">
<header>Item 2</header>
<p>Fusce vel neque sit amet felis fermentum cursus vitae.</p>
<button>Remove from cart</button>
</li>
<li class="selected">
<header>Item 3</header>
<p>Vel neque sit amet vitae suscipit nibh.</p>
<button>Remove from cart</button>
</li>
</ul>

CSS는 다음과 같다:


.cart {
border: 1px solid #ddd;
}
.cart li {
margin-bottom: 10px;
border: 1px solid red;
}
.cart li.selected {
background-color: blue;
}
.cart li.premium {
font-weight: bold;
}

이 레이아웃을 컴포넌트로 변경 할 때 첫 번째 방법은 다음과 같을 수 있다:


class CartList extends React.Component {
onClickItem(event, index) {
this.state.setState({
selected: [
...this.state.selected,
index
]
});
}
onClickRemove () {
// Go to your store and remove item
}
renderCartItem(item, index) {
const isPremium = item.premium ? 'premium' : '';
const isSelected = this.state.selected.indexOf(index) >= 0 ? 'selected' : '';
return (
<li className={`${isPremium} ${isSelected}`} onClick={(event) => this.onClickItem(event, index)}>
<header>{item.title}</header>
<p>{item.description}</p>
<button onClick={this.onClickRemove}>Remove from cart</button>
</li>
);
}
render() {
const { items } = this.props;
return (
<ul className='cart'>
{items.map((item, index) => this.renderCartItem(item, index)}
</ul>
);
}
}

이 목록은 CartList 컴포넌트가 될 것이고 renderCartItem은 선택된 모든 아이템을 보여주는데 필요한 클래스와 목록의 각 항목에 필요한 모든 아이템이있는 li 노드를 렌더링한다.

어쩌면 이 시점에선 문제가 보이지 않을 지 모르지만 추후에 이 결정으로 인한 문제에 직면하게 될 것이다.

몇 주 후, 장바구니에 대한 새로운 요구 사항이 생긴다. 사용자는 이 목록의 아이템을 여러 개 선택할 수 있어야 한다는 것이다. 이 기능을 구현하기 시작하면 아이템 간 커뮤니케이션, 아이템 선택, 이벤트 보내기, 아이템의 클래스 토글 등을 할 수 있는 방법이 필요하다.

잠깐, 이건 미친 짓이다...

어플리케이션의 다른 위치에서 해당 아이템을 다시 사용하는 경우 선택 동작을 원하지 않고 더 많은 조건문을 추가하여 사용되는 모든 동작에 대해 원하는 동작을 선택하기 시작할 수 있다. 주문 목록에 선택 가능한 행동을 추가 하겠는가? 이 중 하나는 CartItem 유형일까?

따라서 아이템에 이 동작을 포함하는 것에 대한 대답은 아마도 "아니"라고 할 수 있다.

이제 컨테이너와 컨텐츠에서 생각해보자. 이 레이아웃에서 컨테이너와 컨텐츠는 무엇일까?


카트 아이템 새 목록

재사용 성 측면에서 컴포넌트 구조를 작성 할 때 각 컴포넌트의 책임이 무엇인지 생각하는 것이 좋다. 컨텐츠를 컨테이너와 구분한다.  아래를 질문해보자:

  • 해당 아이템은 이 목록의 선택 관리에 대한 책임이 있나?
  • 선택을 관리하기 위한 핸들러를 두는 가장 좋은 곳은 어디인가?

생각을 바꿔 다른 시각에서 이것을 바라 볼 수 있다. SelectableList 컴포넌트 및 CartItem을 상상해자. 이 레이아웃으로 새로운 접근법을 확인해보자.


<ul>
<li>
<div>
<header>Item 1</header>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing.</p>
<button>Remove from cart</button>
</div>
</li>
<li class="selected"> <!-- More clear responsibilities (SelectList) -->
<div class="premium"> <!-- More clear responsibilities (CartItem) -->
<header>Item 2</header>
<p>Fusce vel neque sit amet felis fermentum cursus vitae.</p>
<button>Remove from cart</button>
</div>
</li>
<li class="selected">
<div>
<header>Item 3</header>
<p>Vel neque sit amet vitae suscipit nibh.</p>
<button>Remove from cart</button>
</div>
</li>
</ul>

차이점은 미묘하지만, 이렇게 했을 때의 장점을 확인 할 수 있을 것이다.


컴포넌트 스타일

만약에 CSS 마진(margin)을 남용한다면, 이것은 컴포넌트 사이에서 책임을 혼합 하고 있다는 일반적인 신호라고 볼 수 있다. 컨텐츠 컴포넌트는 최대한 확장(expand)되어야하며 주변을 인식해서는 안된다. 컨테이너 컴포넌트는 컨텐츠 컴포넌트 사이의 공간을 관리하는 데 적합한 구성 요소이며 CSS 패딩을 사용하여 이 컴포넌트를 구현할 수 있다.


.selectable-list {
border: 1px solid #ddd;
}
.selectable-list li {
padding-bottom: 10px; /* margin to padding ;) */
}
.selectable-list .selected {
background-color: blue;
}

보다시피, margin-bottom을 장바구니 아이템에서 선택 가능 목록의 padding-bottom으로 옮겼다.

컴포넌트 여백을 패딩으로 변경 해자

이 접근 방식을 사용하기 시작하면 flexbox와 같은 모든 요구 사항에 훨씬 적합하다는 것을 알 수 있다.

컴포넌트 비지니스 로직

DOM의 가장 흥미로운 기능 중 하나이며 가장 오해되는 부분은 이벤트 버블링 이다. 이것이 작동하는 방법과 당신이 알아야 할 주요 주제(topic)가 무엇인지 이해하면 이 모든 새로운 컴포넌트 구조가 빛을 발한다.

이벤트 버블링에 대한 간단한 요약은 stopPropagation 메소드를 호출 할 때까지 DOM 노드에서 발생하는 이벤트가 모든 부모 노드를 통과한다는 것이다.


이를 전제로하면 어플리케이션의 모든 컴포넌트 간에 책임을 공유하는 것은 정말 쉽다.

선택을 관리 할 책임자가 누구인지 생각해 보면 이제는 대답이 정말 쉽다. SelectableList 여야한다.

SelectableList:


class SelectableList extends React.Component {
onClickItem(event, index) {
this.state.setState({
selected: [
...this.state.selected,
index
]
});
}
render() {
return (
<ul>
{items.map((item, index) => {
const isSelected = this.state.selected.indexOf(index) >= 0 ? 'selected' : '';
return (
<li key={index} className={isSelected} onClick={(event) => this.onClickItem(event, index)}>
{renderItem(item)}
</li>
);
})}
</ul>
);
}
}

CartItem:


class CartItem extends React.Component {
onClickRemove () {
// Go to your store and remove item
}
render() {
const { item } = this.props;
const isPremium = item.premium ? 'premium' : '';
return (
<div className={isPremium}>
<header>{item.title}</header>
<p>{item.description}</p>
<button onClick={this.onClickRemove}>Remove from cart</button>
</div>
);
}
}

이것의 큰 장점은 이제 책임이 확실히 분명하고 두 컴포넌트가 서로 충돌하지 않고 확장 할 수 있다는 것이다. SelectableList에 다중 선택을 추가 하고 싶다면 바로 추가 하면 된다. CartItem에 새로운 것을 추가하고 있나? 문제 없다.

컴포넌트 로직과 관련하여, 이벤트 버블링을 사용하여 각 이벤트의 소유자가 어플리케이션에서 생성되는 방식을 결정하자. 이벤트가 상위 컴포넌트를 통과해서는 안된다고 생각되면 stopPropagation 메소드를 사용하여 이벤트를 중지하자.

컴포넌트의 특정 영역을 사용하지 않도록 설정하려는 경우 pointer-events 속성을 사용하여 클릭 할 수 있는 항목을 보다 잘 제어 할 수 있다.

결론

여려가지 사례를 통해 이러한 예제를 추정 할 수 있다. 그 결과 재사용 가능한 일반 컴포넌트 집합과 비즈니스 로직에 연결된 컴포넌트 집합이 될 것이다.

이 일반 컴포넌트 집합을 사용하면 어플리케이션의 접근성(accessibilit)을 보다 쉽게 적용 할 수 있다. 이러한 방식으로 많은 부분에서 어플리케이션을 향상시킬 수 있다.

새로운 컴포넌트를 생성 할 때마다 컨테이너와 컨텐츠에 대해서생각해보자. 그러면 최상의 결과를 얻을 수 있을 것이다!



이 글은 Ferran Basora의 Content vs Container를 번역한 글 입니다. 원문은 아래에서 확인 할 수 있습니다.


리뷰