
themeQuartz.withParams를 사용하는 방법과 SCSS 모듈을 사용하는 방법이 있다.withParams로, 파라미터에 없는 세부 요소는 SCSS로 처리했다. withParams는 공식 파라미터라 버전 업에 안전하지만, SCSS 내부 클래스 지정은 버전 업에 취약하다.:global로 이름 변환을 막아야 한다.:last-child로 제거했더니 컬럼 가상화 때문에 마지막이 아닌 중간 컬럼의 구분선이 사라지는 이슈가 있었다.간단한 데이터 조회 테이블만 보여주는 화면에서는 디자인 시스템 테이블을 사용하고 있었다. 하지만 새롭게 구성하는 화면에서는 테이블에 추가적인 복잡한 기능이 요구되었고, Enterprise로 결제해서 사용하던 AG Grid를 기존 디자인 시스템 테이블과 동일하게 커스텀하여 사용하기로 했다. AG Grid를 사용해도 이질감이 느껴지지 않게 동일한 사용성을 제공해야 했다.
이번 작업을 하며 AG Grid를 처음 사용해보았다. 작업하면서 정보가 많지 않아서 여러 시행착오를 겪었는데, 혹시나 누군가에게 도움이 될까 싶어 정리해보려고 한다.
작업 내용이 많아서 총 5편으로 나누어 글을 작성해보겠다. 이 글은 그 중 첫번째로, AG Grid의 스타일을 맞추는 작업에 관한 글이다. 폰트, 색상, 여백, 정렬, 스크롤바 모양, 셀 구분선, 말줄임, hover 효과까지 디자인 시스템 테이블과 동일하게 맞추기 위해 커스텀한 작업 기록을 담았다.
AG Grid 스타일을 설정할 수 있는 방법은 크게 두 가지로 themeQuartz.withParams와 SCSS 모듈이 있다. 각각 어떻게 사용했는지 정리해보겠다.
withParams는 AG Grid가 공식적으로 노출한 디자인 토큰에 값을 넣는 방식이다.
export const AGGRID_DEFAULT_THEME = themeQuartz.withParams({
fontSize: 16,
fontFamily: 'pretendard-font',
borderColor: '#edeff2',
wrapperBorderRadius: 12,
headerHeight: 56,
headerFontWeight: 'bold',
headerTextColor: '#1e1e23',
headerBackgroundColor: '#f6f8fa',
cellTextColor: '#1e1e23',
cellHorizontalPadding: 12,
rowHoverColor: '#f3f5f7',
selectedRowBackgroundColor: '#eefff2',
rangeSelectionBackgroundColor: '#f3f5f7',
rangeSelectionBorderColor: '#007eff',
});
해당 속성들은 AG Grid가 직접 커스텀하도록 정해둔 공개 파라미터다. 헤더 배경색, 헤더 높이, 행 호버 색, 선택된 행 배경색 등을 파라미터 이름으로 값을 넣어 지정한다.
AG Grid 옵션으로 커버되는 것은 withParams로, 옵션에 없는 것만 SCSS로 처리한다. 이 순서에는 이유가 있다. withParams는 공식 보장 파라미터라 버전이 올라가도 안정적이지만, SCSS 내부 클래스 지정은 클래스 이름이나 구조가 바뀌면 깨질 수 있다는 위험성이 있다. 안전한 쪽을 먼저 쓰고 SCSS는 위험을 감수하고 쓰는 것이다.
SCSS 모듈은 파라미터에 없는 속성을 커스텀할 때 사용한다. AG Grid의 실제 DOM 클래스를 직접 지정해서 스타일을 넣는 방식이다.
일반적으로 스타일을 주입하는 방식과 동일하다. 해당 프로젝트는 SCSS 모듈을 사용하는 환경이라 SCSS를 사용했고, 작업하는 프로젝트의 스타일링 규칙에 맞게 작성해주면 된다.
단, SCSS 모듈을 사용할 땐 주의할 점이 하나 있다. SCSS 모듈은 원하는 컴포넌트에만 스타일이 적용되게 하기 위해서 빌드 시 클래스 이름을 고유한 이름으로 변경한다. 내가 .cell이라고 쓰면 실제로는 .cell_a1b2c3 같은 이름으로 변경하여 적용된다.
AG Grid 내 요소들의 클래스를 .ag-header-cell이나 .ag-cell-value으로 지정하여 설정해주려고 해도, .ag-header-cell_a1b2c3 같은 이름으로 바뀌어 실제 AG Grid 요소와 매칭되지 않는 문제가 있다.
이걸 막는 것이 :global이다. :global로 감싼 범위 안의 클래스는 이름을 바꾸지 않고 그대로 유지한다.
:global {
.ag-header-cell { ... }
.ag-cell-value { ... }
.ag-checkbox { ... }
}
:global 안에서는 .ag-header-cell이 그대로 .ag-header-cell로 남아 AG Grid의 실제 요소에 적용된다. AG Grid처럼 외부 라이브러리가 만든 클래스를 SCSS 모듈에서 지정하려면 이 방식이 필요하다.
themeQuartz.withParams로 적용이 가능한 부분은 적용해주었고, 그외 세세한 설정들은 SCSS 모듈을 이용했다. -webkit-scrollbar 선택자를 이용하여 6px 두께에 둥근 모양으로 스크롤바를 커스텀해주는 작업부터, 폰트와 hover 효과도 여기서 적용해주었다.
.ag-body-horizontal-scroll-viewport {
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
height: 6px;
background: $defign_gray250;
border-radius: 100px;
}
}
조금 더 복잡한 제어가 필요한 케이스는 아래에서 따로 다루어보겠다.
셀 내용이 길어지면 2줄까지만 보여주고, 나머지는 말줄임 되도록 해주었다.
.ag-cell-value {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
문제는 숫자 셀이었다. 숫자가 들어간 셀은 2줄이 아니라 한 줄 말줄임 처리가 필요했다. 그래서 처음에는 가장 흔한 방식인 white-space: nowrap으로 처리했는데, 예상과 다르게 '...' 처리가 되지 않고 글씨가 그대로 잘려서 나왔다.
원인은 컬럼 정의 쪽에 있었다. 일부 컬럼에 wrapText: true가 개별로 걸려 있으면 그 컬럼만 줄바꿈이 허용되어 2줄로 늘어난다. AGGRID_DEFAULT_COL_DEF에는 없지만 특정 컬럼 정의에만 개별 설정되어 있는 경우, 또는 링크와 툴팁을 적용한 컬럼의 cellRenderer 내부에서 줄바꿈을 허용하는 스타일이 들어간 경우 이렇게 된다.
여기서 한 가지 걸렸던 것은 valueFormatter와 cellRenderer의 우선순위다. 숫자 포맷을 valueFormatter로 넣고 cellRenderer도 같이 지정했더니 포맷이 먹지 않았다. cellRenderer가 있으면 valueFormatter는 무시된다. cellRenderer의 value에는 raw 데이터가 그대로 들어오기 때문에, 포매팅이 필요하면 cellRenderer 안에서 직접 해야 한다.
그래서 숫자 셀은 cellRenderer로 한 겹 싸서 그 안에서 한 줄 말줄임을 처리하는 방식으로 변경해주었다.
import type { ColDef, ICellRendererParams } from 'ag-grid-community';
const amountCellConfig: ColDef = {
wrapText: false,
autoHeight: false,
cellRenderer: ({ value }: ICellRendererParams) => (
<div className="ag-cell-one-line">
<span>{value === 0 ? '0' : `${value.toLocaleString('ko-KR')}원`}</span>
</div>
),
};
// 컬럼 정의에서 재사용
{ field: 'totalAmount', headerName: '총 금액', ...amountCellConfig },
{ field: 'supplyAmount', headerName: '공급가액', ...amountCellConfig },
{ field: 'vatAmount', headerName: '부가세', ...amountCellConfig },
공통 설정 객체로 빼두면 숫자 컬럼마다 스프레드로 재사용할 수 있다.
.ag-cell-value {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
한 줄 말줄임 자체는 cellRenderer가 감싼 요소에 2줄 말줄임과 같은 방식으로 -webkit-line-clamp: 1을 적용했다.
컬럼 헤더 사이에 위아래 여백이 있는 세로 구분선이 필요했다. 헤더 셀에 가상 요소로 직접 그려주었다.
.ag-header-cell {
&::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 48%;
background-color: $defign_gray250;
}
&:last-child::after {
display: none;
}
}
마지막 셀에는 구분선이 없어야 하므로 :last-child로 제외했다.
그런데 여기서 이상한 일이 생겼다. 실제로 화면에서 확인해보니 정작 사라진 것은 맨 마지막 컬럼이 아니었다. 중간에 있는 컬럼의 구분선이 없어져 있었다. 컬럼 정의 문서 상 순서가 맨 끝도 아니었고, 그리드 내 가로 스크롤을 움직이며 확인해보니 엉뚱한 컬럼에 구분선이 사라지는 현상을 발견했다.
해당 문제를 해결하려고 검색하며 찾아본 결과, 핵심은 :last-child가 컬럼 순서 기준이 아니라 DOM 순서 기준으로 적용된다는 것이었다.
AG Grid는 컬럼 가상화(column virtualization)를 한다. 가로 스크롤이 있는 그리드에서 화면에 보이는 컬럼만 DOM에 렌더링한다. 스크롤 위치에 따라 오른쪽 끝 컬럼들은 아예 DOM에 없을 수 있다. 그래서 지금 화면 기준으로 렌더된 헤더 셀 중 DOM상 마지막 요소가 :last-child에 걸리게 되는 것이다.
즉, 화면에서 구분선이 사라진 컬럼은 진짜 마지막 컬럼이라서 걸린 게 아니라, 현재 스크롤 위치 기준으로 그 셀이 DOM 트리의 마지막 자식이라서 걸리게 된 것이다. 스크롤하면 걸리는 컬럼이 계속 바뀌게 된다.
덧붙여, 왼쪽 고정 컬럼은 별도 컨테이너(.ag-pinned-left-header)에 들어간다. 그래서 :last-child도 이 컨테이너에 각각 적용된다. 고정 헤더의 마지막 셀에도 구분선이 나오면 안 돼서, 이쪽 역시 별도로 제거해야 했다.
컬럼 정의에서 실제 마지막 컬럼에 headerClass를 붙여 논리적으로 마지막임을 명시해주었다.
{
field: 'lastColumn',
headerName: '마지막 컬럼',
headerClass: 'ag-header-cell--last',
// ...
}
.ag-header-cell--last::after {
display: none;
}
:last-child 대신 실제 마지막 컬럼을 명시적으로 타겟팅해서 스타일을 적용했고, 이러면 DOM 순서나 가상화와 무관하게 논리적으로 마지막인 컬럼에만 확실히 적용된다.
col-id로 마지막 컬럼을 직접 지정하는 방법도 있다. 다만 컬럼이 조건부로 노출되거나 순서가 유동적이면 col-id 하드코딩은 취약하다. 그래서 컬럼 정의에서 headerClass를 붙이는 방식을 택했다.
SCSS로 내려간 스타일은 대부분 .ag-header-cell, .ag-cell-value, .ag-body-horizontal-scroll-viewport 같은 AG Grid 내부 클래스에 의존한다. 이 클래스들은 AG Grid가 공식적으로 지정을 보장한 것이 아니다. 버전이 올라가면서 클래스 이름이나 DOM 구조가 바뀌면 이 스타일들은 적용되지 않게 된다.
분명 설정을 적용한 것 같은데 예상과 다르다면 버전이 달라져서 적용이 되지 않은 건 아닌지 살펴볼 필요가 있다. 그리고 되도록이면 withParams를 우선적으로 사용하는 게 좋다.
withParams는 AG Grid가 공식 보장하는 파라미터라 안전하고, SCSS 내부 클래스 지정은 버전 업에 취약하다.withParams로 처리하고, 구분선이나 스크롤바처럼 파라미터에 없는 세부만 SCSS로 처리한다.:global로 이름 변환을 막아야 한다.:last-child 같은 DOM 순서 기반 선택자는 컬럼 가상화 때문에 의도치 않은 동작을 일으킨다. 논리적 순서는 col-id나 headerClass로 명시해야 한다.지금 당장 동작하는 코드와 라이브러리의 지원 범위가 달라지면 위험할 수도 있는 코드를 분리해야 한다. 그 예로 위에서 보았듯, :last-child는 지금 화면에서는 맞는 컬럼에 걸릴 수도 있지만 조금이라도 설정이나 작업 환경이 달라지면 다르게 보일 수 있다. 라이브러리가 제공하는 기능을 이용할 땐 내가 100% 예측한대로 움직이지 않을 수 있기 때문에, 내부적으로 어떻게 동작하는지에 대한 이해와 꼼꼼한 테스트가 필요하다.
다음 편에서는 로딩 / 빈 데이터 / 에러 상태를 보여주는 오버레이를 직접 만든 과정에 대해 정리해보려고 한다. AG Grid 내장 오버레이가 왜 부족했고, 어떻게 직접 설정했는지 작성해보겠다.