HTML과 CSS로 해당 사이트의 파란색 부분을 퍼블리싱해야 한다면, 여러분은 어떻게 구현하실 건가요?
보통은 display: flex
나 display: grid
와 함께 gap: 20px
을 주어 리스트 내 아이템 간의 간격을 벌리게 됩니다.
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
ul {
display: flex;
flex-direction: column;
gap: 20px;
}
gap을 사용하면 다른 CSS 프로퍼티인 position: relative
와 position: absolute
를 사용하는 것보다 짧은 코드로 간격을 벌릴 수 있으며,
margin-top: 20px
과 같이 각 아이템마다 간격을 명시하는 코드를 작성해야 하는 불편함도 피할 수 있습니다. (더군다나 margin은 tracking 또한 gap에 비해 어렵습니다)
여러 아이템을 렌더링한다고 가정할 때도 아이템들을 감싸는 리스트에만 해당 속성을 부여함으로 가독성을 높이고 반복되는 코드를 피할 수 있습니다.
저 또한 HTML을 사용하면서 flex
와 gap
프로퍼티를 유용하게 사용하며,
CSS에 존재하는 최고의 프로퍼티라고 생각했습니다.
그런데, 이 프로퍼티들을 사용하며 불편함을 느껴보신 적은 없으신가요?
이 글에서는...
- 간격을 벌려주는 컴포넌트를 통해 gap을 대체하는 방법을 소개합니다.
- React의 컴포넌트를 기반으로 내용을 설명합니다.
위와 같은 UI를 gap
프로퍼티를 통해 구현해 보겠습니다.
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
ul {
display: flex;
flex-direction: column;
gap: 20px;
}
단 세 줄의 CSS만으로 UI를 구현할 수 있습니다.
그런데 만약 같은 목록에서 벌려야 하는 간격이 다른 경우는 어떻게 해야 할까요?
신나게 개발하던 중, 디자인의 변경으로 인해 리스트의 첫 번째 아이템에만 10px만큼의 간격을 벌려야 합니다.
이런 요구사항의 경우, 개발자는 Element를 한 depth 더 래핑하여 10px에 대한 예외 처리를 해 주어야 합니다.
<ul>
<li class="first-list-item">
<hgroup>Item 1</hgroup>
<hgroup>Item 2</hgroup>
</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
ul {
display: flex;
flex-direction: column;
gap: 20px;
}
.first-list-item {
display: flex;
flex-direction: column;
gap: 10px;
}
중복되는 코드도 발생하였고, HTML의 트리 구조도 조금은 복잡해졌지만 나름 유쾌하게 해결했습니다.
그런데 요구사항이 만약 더욱 복잡하게 바뀐다면 어떻게 될까요?
디자인이 변경되어 제일 마지막의 Item의 gap은 40px을 처리해 주어야 합니다.
<ul>
<li class="first-list-item">
<hgroup>Item 1</hgroup>
<hgroup>Item 2</hgroup>
</li>
<li>Item 3</li>
<li class="last-list-item">
<hgroup>Item 4</hgroup>
<hgroup>Item 5</hgroup>
</li>
</ul>
ul {
display: flex;
flex-direction: column;
gap: 20px;
}
.first-list-item {
display: flex;
flex-direction: column;
gap: 10px;
}
.last-list-item {
display: flex;
flex-direction: column;
gap: 40px;
}
어떻게든 처리가 되었지만, 똑같은 세 줄의 CSS 코드가 세 번이나 반복되고 있습니다.
아이템 내 또 다른 아이템을 처리하여 HTML 구조도 훨씬 읽기 복잡해져서, 이제 구조만으로는
정확하게 리스트를 렌더링하고 있다고 한 눈에 알아보기 힘들어졌습니다.
그렇다고 margin으로 이를 처리한다고 해도, margin을 주어야 하는 각 li에 대해 추가적인 CSS를 작성해 주어야 하는 것뿐만 아니라,
만약 요구 사항이 변경되어 코드를 tracking해야 하는 경우 margin의 행방은 gap에 비해 찾기가 어려워집니다.
또한 gap과 다르게 margin을 부여한 li가 CSS Layout 상에서 해당 공간을 차지하고 있기에, 의도치않은 side-effect까지 발생할 수 있습니다.
gap의 한계점은 이렇습니다. 설명드렸던 예제는 비교적으로 간단하지만, 저런 예제의 구조가 끝없이 반복되는 요구 사항을 받았을 경우 개발자는 난처해지기 마련입니다.
그렇다면 이런 문제를 어떻게 해결할 수 있을까요?
어플리케이션을 개발하는 데 사용되는 프레임워크인 Flutter에는 SizedBox라는 위젯을 사용하여
간격을 벌리는 경우가 많습니다.
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ColoredRectangle(),
SizedBox(height: 16.0,),
ColoredRectangle(),
SizedBox(height: 16.0,),
ColoredRectangle(),
],
),
어쩌면 CSS에서도 간격을 벌리는 빈 컴포넌트를 만들어 사용한다면,
여러 Element를 사용하지 않고도 깔끔하게 표현할 수 있지 않을까요?
<ul>
<li>Item 1</li>
<Spacer />
<li>Item 2</li>
<Spacer />
<li>Item 3</li>
</ul>
저는 업무를 진행하면서 기존에 Spacer를 사용하여 간격을 벌리는 방식을 사용하고 있었는데,
이를 더 빠르고 명시적으로 사용하기 위해서 컴포넌트로 만들어보았습니다.
// Before
<div className="h-[42px]" />
// After
<Spacer h42 />
자주 사용되는 컴포넌트인데도 불구하고, div
로 표현된다는 점과 h-[xx]
와 같이 표현해야 한다는 점이 불편했습니다.
그래서 컴포넌트를 만들 때도 height='42px'
과 같은 props로 전달하기보단 매우 빠르게 간격을 표현할 수 있도록 간소화해 보겠습니다.
const App = () => {
return (
<div>
<ul>
<li>Item 1</li>
<Spacer h20 w120 />
<li>Item 2</li>
<Spacer h20 />
<li>Item 3</li>
</ul>
</div>
);
};
const Spacer = ({ ...props }) => {
const space = Object.keys(props)
.map((key) => ({ [key[0]]: `${key.slice(1, key.length)}px` }))
.reduce((acc, obj) => ({ ...acc, ...obj }), {});
return (
<div
style={{
width: space.w ?? 0,
height: space.h ?? 0,
}}
/>
);
};
개발자의 수월함을 위해 props를 skip하고 Spacer에 이름 자체를 전달함으로 간격을 벌릴 수 있게 개발했습니다.
언뜻 보면 위험해 보이지만, 결국 style에서 space 객체의 w와 h를 호출하기에
규약에 맞지 않는 props가 들어올 경우 side-effect를 발생시키지 않습니다.
가로 간격과 세로 간격을 결정하는 것을 w
, h
접두사로 표현하고,
그 뒤 숫자를 바로 px로 적용할 수 있도록 합니다.
<Spacer h20 w120 />
원하던대로 잘 작동하네요!
웬만한 상황에서는 gap을 사용하여 간격을 표현할 수 있지만,
어떤 목록에 한하여 각각의 gap이 다른 요구 사항에서는 Spacer를 사용하여 간격을 표현할 수 있습니다.
gap의 한계로 인해 HTML Element가 차곡차곡 쌓여 고민 중이시라면,
Spacer를 도입해 요구 사항을 해결해 보시는 것을 추천드립니다.
와우 기가막힌 방법이네요 특히
Spacer
에서 props를 저렇게 사용 한 방법은 정말 기가막히고 코가 막히는군요