CSS Grid를 1차원 레이아웃에 활용하기


Flexbox와 Grid

CSS에서 레이아웃을 구성하는 대표적인 방법으로 Flexbox와 Grid가 있다. 두 방법의 대표적인 차이는 Flexbox는 1차원, Grid는 2차원 레이아웃을 정의할 때 사용한다는 것이다. 상황에 따라 선택할 수도 있고 같이 사용하는 경우도 있다.

Flexbox는 기본축과 교차축을 기준으로로 아이템을 배치할 수 있다. 컨테이너 내부의 아이템들이 서로 영향을 주고받으며, 유연하게 공간을 분배한다. 프론트엔드 개발에 많이 사용되는 속성이라 크게 낯설지는 않을 것이다.

Grid는 행(row)과 열(column)로 이루어진 2차원 레이아웃 시스템이다. 즉 X축과 Y축이 있는 것을 말한다. 격자의 각 셀은 독립적이며, 다른 셀에 영향을 주지 않는다.

컴포넌트

그렇다면 항상 1차원은 Flexbox, 2차원 레이아웃은 Grid를 사용하는 것이 옳을까? 당연히 항상 그렇지는 않다. 1차원에서 Grid가 더 유리하다고 느낀 경험이 있어 공유해보려고 한다.

다음과 같은 간단한 Header 컴포넌트를 구현한다고 가정해봅시다.

헤더 이미지

요구사항은 제목이 항상 정중앙에 위치해야 하며, 좌측/우측 액션 버튼은 선택적으로 존재한다.

제목만 있는 페이지, 좌측 액션 버튼만 있는 페이지 등 여러 케이스가 있을 수 있다. 이 때마다 Header 컴포넌트를 만드는 것은 좋지 않다. 하나의 컴포넌트에서 여러 변경 사항에 대응이 가능해야 한다.

여러 가지 방법으로 구현해보자.

1. Flexbox

가장 먼저 떠올릴 수 있는 방법은 Flexbox 레이아웃 시스템을 사용하는 것이다.

export default const Header = ({ children, leftAction, rightAction }) => {
  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
      }}
    >
      <div>{leftAction}</div>
      <h1>{children}</h1>
      <div>{rightAction}</div>
    </div>
  );
};

간단하게 구현이 가능하지만 두 가지 문제가 발생할 수 있다.

첫 번째로는 Flexbox는 같은 컨테이너 내부의 아이템끼리 서로 영향을 준다. 만약 rightAction이 없는 케이스에서는 justify-content: space-between 속성으로 인해 제목이 우측에 배치될 것이다.

헤더 이미지

또한 justify-content: space-between 속성은 아이템 사이 공간의 크기는 동일하게 유지시키기 때문에 제목이 정중앙에 배치되지 않을 수 않다.

헤더 이미지

2. Position Absolute

position: absolute를 적용해 제목을 절대 위치로 정중앙에 고정시키는 방법도 있다.

const Header = ({ children, leftAction, rightAction }) => {
  return (
    <div
      style={{
        position: 'relative'
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
      }}
    >
      <div>{leftAction}</div>
      <h1
        style={{
          position: 'absolute',
          left: '50%',
          transform: 'translateX(-50%)',
        }}
      >
        {children}
      </h1>
      <div>{rightAction}</div>
    </div>
  );
};

이 방법의 장점은 제목이 항상 정중앙에 고정된다. 하지만 제목 길이에 따라 액션 버튼과 겹칠 가능성이 있다. 또한 스타일 속성 사용이 많아지고, 위치를 다시 계산해줘야하는 약간의 번거로움이 있다.

3. CSS Grid

이제 Grid를 활용한 1차원 레이아웃을 정의해보자. 쉽게 1행 3열로 이루어진 2차원 공간으로 생각하면 된다.

const Header = ({ children, leftAction, rightAction }) => {
  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
      }}
    >
      <div>{leftAction}</div>
      <h1>{children}</h1>
      <div>{rightAction}</div>
    </div>
  );
};

grid-template-columns: 'repeat(3, minmax(0, 1fr))'로 3개의 동일한 너비 열을 생성한다. 최소 0, 최대 1fr로 유연하게 대응하기 위해 minmax를 사용했다.

헤더 이미지

Grid의 각 셀은 독립적이므로, leftAction이 없거나 rightAction의 크기가 커져도 제목의 위치는 변하지 않는다. 이제 각자 셀 내부의 위치를 정해주면 된다.

const Header = ({ children, leftAction, rightAction }) => {
  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
      }}
    >
      <div style={{ display: 'flex', justifyContent: 'start' }}>
        {leftAction}
      </div>
      <h1 style={{ textAlign: 'center' }}>{children}</h1>
      <div style={{ display: 'flex', justifyContent: 'end' }}>
        {rightAction}
      </div>
    </div>
  );
};

헤더 이미지

이제 각 영역은 독립적이라 조건부 렌더링 시에도 레이아웃이 깨지지 않는다.

마무리

사실 스타일은 팀 컨벤션 또는 개인의 취향을 많이 따르기에 정답은 없다. 리플로우와 같이 성능에 문제만 주지 않으면 자유롭게 사용해도 된다고 생각한다. 하지만 또 그렇기에 한 번 습관이 되면 동일한 스타일을 적용하게 될 것이다.

원래 Flexbox를 주로 사용했다. 하지만 이번 Grid로 1차원에서 스타일을 할 수 있는 것을 적용해본 경험으로 느낀 것은 크게 두 가지이다.

  1. Flexbox는 다른 항목끼리 영향을 주는 속성이라 의도한대로 보여지지 않을 수 있다.
  2. Grid로 큰 틀을 잡고 개별 항목의 위치를 따로 설정해주면 더 직관적이다.

앞으로 이런 경우라면 Gird 활용을 추천한다.