디자인 시스템 테이블과 동일하게 AG Grid 커스터마이징 하기

진주·2026년 7월 26일

AG Grid 커스텀

목록 보기
1/3
post-thumbnail

🪄 TL;DR

  • 이미 정의된 디자인 시스템을 사용하는 프로젝트에서 복잡한 테이블 구현 요구사항이 생겼고, AG Grid Enterprise를 활용하여 AG Grid 테이블을 기존 디자인 시스템 테이블과 동일한 형태로 커스터마이징했다.
  • AG Grid 커스터마이징 방법은 테마 API인 themeQuartz.withParams와 SCSS 모듈을 사용하는 두 가지 방법이 있다.
  • 파라미터로 설정 가능한 것은 withParams를 사용하고, 파라미터에 없는 세부 스타일은 SCSS로 처리했다.
  • SCSS 모듈 환경에서 AG Grid의 클래스를 지정하려면 :global로 이름 변환을 막아야 한다.
  • 헤더 구분선을 :last-child로 제거했더니, DOM 가상화(DOM Virtualisation) 때문에 DOM 기준 마지막 컬럼에 스타일이 적용되어 의도하지 않은 컬럼의 구분선이 사라지는 문제가 발생했다.

내가 진행하고 있는 프로젝트는 사내 디자인 시스템을 사용하고 있다. 간단한 데이터 조회 화면에서는 기존 디자인 시스템 컴포넌트에 데이터를 주입하여 그대로 사용할 수 있지만, 새롭게 구성하는 화면에서는 복사, 툴팁, 셀 영역 확장 등 추가적인 기능 구현이 요구되었다. 이미 팀에서 결제해서 사용하는 AG Grid Enterprise 권한이 있었기에, 요구사항을 직접 스크립트로 구현하여 사용하기보다 AG Grid를 기존 디자인 시스템의 테이블과 동일한 형태로 커스터마이징하기로 했다. 두 테이블 사이에 이질감이 느껴지지 않게 동일한 사용성을 제공할 수 있도록 신경 써서 작업을 진행했다.

작업하면서 관련 자료가 많지 않아 여러 시행착오를 겪었다. 같은 문제를 겪는 사람에게 도움이 되었으면 하는 마음으로 작업 과정을 정리해보려고 한다.

작업 내용이 많아서 총 5편으로 나누어 글을 작성해보겠다. 이 글은 그 중 첫 번째로, AG Grid의 커스터마이징 작업에 관한 글이다. 폰트, 색상, 여백, 정렬, 스크롤바 모양, 셀 구분선, 말줄임, 호버(hover) 효과까지 디자인 시스템 테이블과 동일하게 맞추기 위해 커스터마이징한 작업 기록을 정리해보았다.

withParams와 SCSS로 스타일 맞추기

AG Grid 커스터마이징 방법은 크게 두 가지로 themeQuartz.withParams와 SCSS 모듈이 있다.

withParams

withParams는 AG Grid가 공식적으로 노출한 디자인 토큰에 값을 넣는 방식이다.

export const AGGRID_DEFAULT_THEME = themeQuartz.withParams({
  fontSize: 16,
  fontFamily: 'Pretendard',
  borderColor: '#edeff2',
  wrapperBorderRadius: 12,
  modalOverlayBackgroundColor: 'transparent',

  headerHeight: 57,
  headerFontWeight: 'bold',
  headerTextColor: '#1e1e23',
  headerBackgroundColor: '#f6f8fa',
  headerColumnResizeHandleWidth: 0,
  headerColumnResizeHandleHeight: 0,

  cellTextColor: '#1e1e23',
  cellHorizontalPadding: 12,
  rowVerticalPaddingScale: 2.6,

  rowHoverColor: '#f3f5f7',
  selectedRowBackgroundColor: '#eefff2',
  selectCellBorder: 'transparent',
  rangeSelectionBackgroundColor: '#f3f5f7',
  rangeSelectionBorderColor: '#007eff',
});

해당 속성들은 AG Grid가 직접 커스터마이징하도록 정해둔 공개 파라미터다. 헤더 배경색, 헤더 높이, 행 호버 색, 선택된 행 배경색 등을 파라미터 이름으로 값을 넣어 지정한다. themeQuartz.withParams()로 생성한 테마는 내부적으로 CSS Custom Properties(CSS 변수) 형태로 변환되어 적용된다. 가능한 파라미터의 종류는 Theme Parameters Reference에서 확인할 수 있고, 사용법은 Theme Parameters를 참고하면 된다.

AG Grid에서 제공하는 기본 테마를 활용할 수도 있는데, Theme Builder를 이용해 직접 적용 및 커스텀 해볼 수 있다.

withParams 방식은 AG Grid에서 공식적으로 지원하는 API이기 때문에 내부 클래스 구조가 변경되더라도 비교적 안정적으로 동작한다. 하지만 정해진 파라미터가 제한적이기 때문에 세밀한 커스터마이징이 필요하다면 SCSS를 함께 사용하는 것이 사실상 필수였다.

SCSS 모듈과 :global

SCSS 모듈은 파라미터에 없는 속성을 커스터마이징할 때 사용한다. AG Grid가 렌더링하는 실제 DOM 클래스에 직접 스타일을 적용하는 방식이다. 일반적인 CSS 스타일링과 동일한 방식으로 작성하면 된다.

해당 프로젝트는 SCSS 모듈을 사용하는 환경이라 SCSS를 사용했고, 프로젝트의 스타일링 규칙에 맞춰 일반적인 SCSS 코드와 동일하게 작성하면 된다.

다만 SCSS 모듈 환경에서는 한 가지 주의할 점이 있다. AG Grid처럼 외부 라이브러리가 만든 클래스를 SCSS 모듈에서 지정하려면 :global을 사용해야 한다. SCSS 모듈은 원하는 컴포넌트에만 스타일이 적용되게 하기 위해서 빌드 시 클래스 이름을 고유한 이름으로 변경한다. 내가 .cell이라고 쓰면 실제로는 .cell_a1b2c3 같은 이름으로 변경하여 적용되는 것이다. :global로 감싼 범위 안의 클래스는 이름을 바꾸지 않고 그대로 유지하며 적용된다.

:global {
  .ag-header-cell { ... }
  .ag-cell-value { ... }
  .ag-checkbox { ... }
}

:global 안에서는 .ag-header-cell이 변환되지 않고 .ag-header-cell 그대로 AG Grid의 실제 요소에 적용된다. AG Grid처럼 외부 라이브러리가 만든 클래스를 SCSS 모듈에서 지정하려면 이 방식이 필요하다.

SCSS 모듈로 실제 스타일 적용하기

themeQuartz.withParams로 적용이 가능한 부분을 먼저 적용한 후, 세세한 디자인 설정들을 SCSS 모듈을 이용해 커스터마이징하였다.

스크롤바 커스터마이징

이번에 사용한 AG Grid 버전에서는 withParams만으로 원하는 형태의 스크롤바를 설정하기 어려웠다. 따라서 ::-webkit-scrollbar 계열 선택자를 이용해 두께와 모양을 직접 조정했다.

:global {
  .ag-body-horizontal-scroll {
    position: absolute;
    bottom: 0;
    padding: 6px;
    z-index: 2;

    .ag-horizontal-left-spacer {
      height: 0;
    }

    .ag-body-horizontal-scroll-viewport {
      bottom: 12px;

      &::-webkit-scrollbar {
        height: 6px;
      }

      &::-webkit-scrollbar-thumb {
        height: 6px;
        background: #d9dde3;
        border-radius: 100px;
      }

      &::-webkit-scrollbar-track {
        height: 6px;
        background: transparent;
      }
    }
  }
}

줄 수에 따른 별도 말줄임 처리

셀 내용이 길어지면 최대 두 줄까지만 보여주고, 나머지는 말줄임되도록 처리했다.

:global {
  .ag-cell-value {
    display: -webkit-box;
    line-clamp: 2;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: 1.4;
    letter-spacing: -0.3px;
    white-space: normal;
    overflow: hidden;
    font-family: 'Pretendard', sans-serif !important;
    text-align: center;
  }
}

위 스타일을 적용하자 셀 내용이 최대 두 줄까지만 표시되고, 초과하는 내용에는 말줄임이 정상적으로 적용되었다.

크롬에서는 의도한 형태로 렌더링되었지만, 사파리에서는 셀에 지정한 폰트가 정상적으로 적용되지 않는 문제가 있었다. AG Grid의 기본 스타일보다 우선 적용되도록 셀에 font-family를 명시적으로 지정했다.

그런데, 문제는 숫자 컬럼이었다. 숫자가 들어간 컬럼은 두 줄이 아닌 한 줄 말줄임 처리가 필요했다. 그래서 처음에는 가장 흔한 방식인 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;으로 처리했는데, 일반적인 말줄임(...)이 적용되지 않고 텍스트가 중간에서 잘린 것처럼 렌더링되었다.

원인을 확인해보니 컬럼 정의 쪽에 문제가 있었다. 컬럼 너비 조정이 가능했기 때문에, 컬럼 너비가 줄어들어도 레이아웃이 깨지지 않게 처리하기 위해 모든 컬럼에 최대 두 줄 말줄임 적용이 필요했다. 그래서 AGGRID_DEFAULT_COL_DEF에서 전체적으로 wrapText: true를 걸어두었고, 숫자 컬럼에는 별도의 클래스를 지정해 한 줄 말줄임 처리를 하려고 했지만 공통으로 적용된 스타일과 충돌하여 의도한 동작이 나오지 않았다.

말줄임이 정상적으로 동작하려면 부모 요소의 너비가 명확해야 했다. cellRenderer를 이용하여 <div className="ag-cell-one-line">라는 별도의 wrapper 요소를 하나 두고, 그 안에서 말줄임이 되도록 처리하였다.

import type { ColDef, ICellRendererParams } from 'ag-grid-community';

export const amountCellConfig: ColDef = {
  wrapText: false,
  autoHeight: false,
  cellRenderer: ({ value }: ICellRendererParams) => (
    <div className="ag-cell-one-line-wrap">
      <span className="ag-cell-one-line">{value === 0 ? '0' : `${value.toLocaleString('ko-KR')}원`}</span>
    </div>
  ),
};
// 컬럼 정의에서 재사용
{ field: 'totalAmount', headerName: '총 금액', ...amountCellConfig },
{ field: 'saleAmount', headerName: '할인 금액', ...amountCellConfig },
{ field: 'vatAmount', headerName: '부가세', ...amountCellConfig },

공통 설정 객체로 만들어 숫자 컬럼마다 스프레드 문법으로 재사용했다.

:global {
  .ag-cell-one-line-wrap {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;

    .ag-cell-one-line {
      display: block;
      width: 100%;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
  }
}

헤더 셀 사이 구분선

컬럼 헤더 사이에 위아래 여백이 있는 세로 구분선이 필요했다. 헤더 셀에 가상 요소를 이용해 직접 그렸다.

:global {
  .ag-pinned-left-header {
    .ag-header-row {
      .ag-header-cell:last-child {
        &::after {
          display: none;
        }
      }
    }
  }
  
  .ag-header-cell {
    &::after {
      content: '';
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      width: 1px;
      height: 48%;
      background-color: #d9dde3;
    }
    
    &:last-child::after { // 수정 전
      display: none;
    }
  }
}

마지막 셀에는 구분선이 없어야 하므로 :last-child로 제외했다.

그런데 예상하지 못한 문제가 발생했다. 실제로 화면에서 확인해보니 정작 사라진 것은 맨 마지막 컬럼이 아니었다. 스크롤 하면서 확인해보니 중간 컬럼의 구분선이 간헐적으로 사라지는 현상을 발견했다.

DOM 가상화

해당 문제를 해결하려고 검색하며 찾아본 결과, 핵심은 :last-child가 컬럼 순서가 아니라 DOM 순서를 기준으로 적용된다는 점이었다.

AG Grid는 성능 최적화를 위해 DOM 가상화(DOM Virtualisation)를 사용한다. 즉, 가로 스크롤이 있는 그리드에서 현재 화면에 보이지 않는 컬럼은 DOM에 생성하지 않고, 화면에 보이는 컬럼만 DOM에 렌더링하는 것이다. 스크롤 위치에 따라 오른쪽 끝 컬럼들은 아직 DOM에 만들어지지 않은 상태일 수 있다. 따라서 특정 시점에는 실제 마지막 컬럼이 아니더라도 DOM 상에서는 마지막 자식이 될 수 있다.

내가 원하는 것은 DOM 상 마지막 요소가 아니라, 논리적으로 마지막 컬럼의 우측 구분선만 제거하는 것이었다. col-id를 이용해 특정 컬럼을 직접 선택하는 방법도 있지만, 매번 col-id를 지정해주는 방식보다 컬럼 정의에 headerClass를 추가하는 방법을 이용했다. 실제 마지막 컬럼에 headerClass: 'ag-header-cell--last'를 지정함으로써 논리적으로 마지막 컬럼임을 명시했다.

{
  field: 'lastColumn',
  headerName: '마지막 컬럼',
  headerClass: 'ag-header-cell--last',
  // ...
}
:global {
  .ag-header-cell {
    &::after {
      content: '';
      position: absolute;
      top: 50%;
      width: 1px;
      height: 48%;
      background-color: #d9dde3;
      transform: translateY(-50%);
    }

    &.ag-header-cell--last::after { // 추가
      display: none;
    }
  }
}

:last-child 대신 실제 마지막 컬럼을 명시적으로 타겟팅하면 DOM 순서나 가상화와 관계없이 원하는 결과를 얻을 수 있다.

결론

정리하자면, AG Grid의 스타일을 커스터마이징하는 두 가지 방법은 안정성 측면에서 차이가 있다. withParams는 AG Grid가 공식적으로 제공하는 API이므로 내부 클래스를 직접 지정하는 방식보다 상대적으로 안정적이지만, SCSS로 내부 클래스를 지정하는 방식은 버전 변경에 영향을 받을 수 있다. 그렇기 때문에 공식 파라미터로 설정 가능한 것은 최대한 withParams를 사용하고, 세부적인 스타일만 SCSS로 처리했다. 설정했는데도 기대한 결과가 나오지 않는다면 사용 중인 AG Grid 버전에서 해당 파라미터를 지원하는지 먼저 확인할 필요가 있다.

또한, 라이브러리가 제공하는 기능은 항상 예상한 방식으로 동작하지 않을 수 있기 때문에, 내부 동작을 이해하고 다양한 상황에서 테스트하는 과정 역시 중요하다는 것을 느꼈다. 위에서 보았듯 :last-child는 특정 DOM 구조에서는 의도한 컬럼에 적용되는 것처럼 보이지만, 가로 스크롤에 따라 렌더링되는 컬럼이 달라지면 다른 컬럼에 적용될 수 있다. 나 역시 처음에는 문제를 발견하지 못했지만, 여러 스크롤 위치에서 컬럼을 확인하는 과정에서 구분선이 간헐적으로 사라지는 현상을 발견했다.

다음 편에서는 로딩 / 빈 데이터 / 에러 상태를 보여주는 오버레이를 직접 만든 과정에 대해 정리해보려고 한다. AG Grid의 기본 오버레이가 왜 요구사항을 충족하지 못했는지, 어떻게 직접 설정했는지 작성해보겠다.

profile
UX Engineer

1개의 댓글

comment-user-thumbnail
2026년 8월 5일

글 잘 읽었습니다! AG Grid를 디자인 시스템에 맞추는 과정에서 공식 테마 파라미터인 withParams를 우선 사용하고, 지원되지 않는 세부 스타일만 SCSS로 보완한 내용에 대해 새롭게 알게되었습니다.

덕분에 글을 읽고 컬럼 가상화와 DOM 렌더링 방식도 조금 더 찾아보았습니다. AG Grid의 내부 동작이랑 headerClass 내용 상당히 복잡한데 대단하네요!! 다음 오버레이 커스터마이징 글도 기대하겠습니다!

답글 달기