
No Rows Overlay 영역을 활용했다.autoHeight를 사용한 첫 렌더링에서 발생한 행 높이 점프는 SCSS 패딩을 제거하고 테마 파라미터인 rowVerticalPaddingScale을 적용해 해결했다.그리드가 정상적으로 표시되지 않는 상황을 세 가지 상태로 구분해 보여줘야 했다.

데이터를 불러오는 중에는 로딩 화면을, 조회 결과가 없을 때는 빈 데이터 안내 화면을, API 요청이 실패했을 때는 에러 화면을 표시해야 했다.
AG Grid는 로딩과 빈 데이터 상태를 위한 내장 오버레이를 제공한다. AG Grid의 내장 오버레이 대신 프로젝트에서 공통으로 사용하는 Loading, Error 컴포넌트를 연결할 수 있지만, 이번 작업에서는 단순히 컴포넌트를 교체하는 것만으로는 요구사항을 충족하기 어려웠다.
처음에는 AG Grid의 내장 오버레이에 프로젝트 공통 Loading, Error 컴포넌트를 지정했다.

하지만 디자인 시안에 맞춰 상태별 높이와 여백을 조정하려고 하자 의도한 형태로 적용되지 않았다. 내장 오버레이가 자체적인 위치와 크기 규칙을 가지고 있어, 컴포넌트 내부만 수정하는 방식으로는 그리드 전체의 레이아웃을 세밀하게 제어하기 어려웠기 때문이다.
오버레이에 표시되는 내용뿐 아니라 높이와 배치까지 직접 제어해야 했기 때문에, 상태에 따라 구현 방식을 나누기로 했다.
로딩 상태는 AG Grid의 내장 오버레이를 사용하지 않고, 별도의 컴포넌트를 그리드 위에 직접 올리는 방식으로 구현했다.
먼저 내장 로딩 오버레이가 자동으로 표시되지 않도록 loading을 false로 지정했다.
<AgGridReact
loading={false}
// ...
/>
이후 isLoading 상태에 따라 별도의 로딩 오버레이를 렌더링했다.
{isLoading && (
<div className={cx('loadingOverlay')}>
<Loading />
</div>
)}
.loadingOverlay {
position: absolute;
top: var(--ag-header-height, 58px);
right: 0;
bottom: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
height: 500px;
box-sizing: border-box;
background: #fff;
border: 1px solid #edeff2;
border-radius: 0 0 12px 12px;
}
isLoading이 true일 때만 로딩 컴포넌트를 그리드 위에 표시했다. 프로젝트 공통 Loading 컴포넌트를 사용하되, 높이와 여백은 loadingOverlay 클래스에서 직접 지정해 시안에 맞췄다.
그리드 헤더를 가리지 않도록 top 값을 --ag-header-height만큼 내려준 것이 핵심이었다.
빈 데이터와 에러 상태는 AG Grid의 No Rows Overlay 영역을 활용했다. 두 상태 모두 행이 표시되지 않는다는 공통점이 있으므로, 에러 여부에 따라 해당 영역에 넣을 컴포넌트만 전환했다.
<AgGridReact
noRowsOverlayComponent={
isError ? GridErrorOverlay : GridNoRowsOverlay
}
// ...
/>
const GridErrorOverlay = () => (
<Error type={ERROR_TYPE.API_ERROR} />
);
const GridNoRowsOverlay = () => (
<Error type={ERROR_TYPE.RESULT_EMPTY} />
);
프로젝트 공통 Error 컴포넌트를 재사용하고, 에러와 빈 데이터 상태에 맞는 타입만 각각 전달했다.
오버레이가 표시될 때 본문 영역의 높이가 지나치게 줄어들지 않도록 최소 높이도 함께 지정했다.
:global {
.ag-root:has(.ag-overlay:not(.ag-hidden)) {
.ag-center-cols-container,
.ag-center-cols-viewport {
min-height: 446px !important;
}
}
}
오버레이가 활성화된 경우에만 그리드 본문 영역에 min-height를 적용해, 빈 데이터와 에러 화면도 디자인 시안과 동일한 높이를 유지하도록 했다.
다만 :has()는 브라우저 지원 범위를 확인한 뒤 사용하는 것이 좋다. 지원 대상 브라우저에 제약이 있다면 오버레이 상태에 따라 별도의 클래스명을 추가하는 방식도 고려할 수 있다.
어떤 오버레이를 보여줄지는 로딩, 에러, 데이터 상태에 따라 결정했다.
useEffect(() => {
const gridApi = refGrid.current?.api;
if (!gridApi || isLoading) return;
const isNoData =
Array.isArray(rowData) && rowData.length === 0;
if (isError || isNoData) {
gridApi.showNoRowsOverlay();
return;
}
gridApi.hideOverlay();
}, [isError, isLoading, rowData]);
로딩 오버레이는 컴포넌트의 반환부에서 isLoading 조건에 따라 별도로 렌더링하므로, 이 useEffect에서는 로딩 중일 때 바로 반환했다.
로딩이 끝난 뒤 에러가 발생했거나 데이터가 없다면 showNoRowsOverlay()를 호출하고, 정상적으로 데이터가 존재하면 hideOverlay()로 오버레이를 감췄다.
로딩은 그리드 외부에서 직접 렌더링하고, 빈 데이터와 에러는 AG Grid의 오버레이 영역을 재사용하는 방식으로 역할을 나눴다.
이 코드는 당시 프로젝트에서 사용한 AG Grid 버전을 기준으로 작성했다. 사용하는 버전에 따라 최신 오버레이 API의 사용 방법이 달라질 수 있으므로 공식 문서를 함께 확인하는 것이 좋다. 최신 버전에서는 통합 overlayComponent와 activeOverlay API도 제공한다.
QA 과정에서 새로운 요구사항이 추가됐다.
그리드 위에 있는 버튼도 로딩·빈 데이터·에러 상태에 따라 함께 표시되거나 사라져야 했다. 즉, 그리드의 데이터 영역뿐 아니라 버튼을 포함한 페이지 영역 전체가 상태에 맞춰 전환되어야 했다.
이 요구사항은 그리드 내부 오버레이만으로 처리하기 어려웠다. 그리드 바깥의 버튼은 공통 그리드 컴포넌트가 제어할 수 있는 영역이 아니기 때문이다.

그리드는 여러 화면에서 공통으로 사용하는 컴포넌트다. 따라서 그리드 자체는 로딩·빈 데이터·에러 상황에서 자신의 데이터 영역을 안전하게 표현하는 책임을 유지하도록 했다.
반면 해당 사용처에서는 버튼을 포함한 페이지 레이아웃 전체가 상태에 따라 달라져야 했다. 이 부분은 공통 그리드가 아니라 그리드를 사용하는 화면의 책임으로 두었다.
그리드는 자신의 데이터 영역을 책임지고, 사용처는 버튼을 포함한 페이지 레이아웃 전체의 상태를 책임진다.
사용처에서는 다음과 같이 상태별 조건을 계산했다.
export function PaymentDataGrid({
isLoading,
isError,
totalCount,
children,
// ...
}: PaymentDataGridProps) {
const showLoading = isLoading;
const showError = !isLoading && isError;
const showEmpty =
!isLoading && !isError && totalCount === 0;
const showContent =
!isLoading && !isError && totalCount > 0;
return (
<div className={cx('article')}>
{showLoading && (
<div className={cx('section-loading')}>
<Loading text="잠시만 기다려 주세요" />
</div>
)}
{showError && (
<div className={cx('section-error')}>
<Error
type={ERROR_TYPE.API_ERROR}
mergeOption={{ button: undefined }}
/>
</div>
)}
{showEmpty && (
<div className={cx('section-error')}>
<Error
type={ERROR_TYPE.RESULT_EMPTY}
mergeOption={{ button: undefined }}
/>
</div>
)}
{showContent && (
<>
{/* 버튼 및 그리드 관련 컴포넌트 */}
</>
)}
<div
className={cx('section-data', {
hidden: !showContent,
})}
>
{children}
</div>
</div>
);
}
코드만 보면 그리드 내부와 사용처에서 상태를 중복으로 판별하는 것처럼 보일 수 있다. 하지만 두 로직이 책임지는 대상은 서로 다르다.
그리드 내부 오버레이는 공통 그리드가 단독으로 사용되는 다른 화면을 위한 기본 안전망이다. 사용처의 상태 분기는 이 화면에만 존재하는 버튼 연동 요구사항을 처리한다.
공통 컴포넌트가 스스로를 보호하는 계층과 특정 화면이 자신의 레이아웃을 책임지는 계층을 분리해 두 요구사항을 모두 충족했다.
autoHeight 첫 렌더링의 행 높이 점프 해결하기
오버레이 작업을 마친 뒤 행 높이에서도 문제가 하나 발견됐다.
셀의 상하 패딩을 SCSS로 지정하고 있었는데, autoHeight가 적용된 행을 처음 렌더링할 때 짧은 순간 높이가 달라지는 현상이 나타났다.
처음에는 기본 높이를 기준으로 행이 렌더링되고, 이후 SCSS로 지정한 패딩이 적용되면서 행 높이가 다시 늘어났다. 이 차이로 인해 화면에서는 행 높이가 순간적으로 바뀌었고, 전체 렌더링이 버벅이는 것처럼 보였다.
해결을 위해 SCSS에서 지정하던 상하 패딩을 제거하고, 테마 파라미터인 rowVerticalPaddingScale을 사용했다.
export const AGGRID_DEFAULT_THEME =
themeQuartz.withParams({
// ...
// autoHeight 첫 렌더링의 높이 점프 방지
// 두 줄 셀의 상하 여백을 맞춘 실측값
rowVerticalPaddingScale: 2.6,
});
rowVerticalPaddingScale은 고정된 픽셀 값이 아니라, 테마의 기본 spacing을 기준으로 행의 세로 패딩을 조정하는 배율이다.
두 줄로 표시되는 셀의 높이와 상하 여백이 시안에 맞도록 값을 조정했고, 여러 차례 테스트한 뒤 2.6을 적용했다.
SCSS로 렌더링 이후의 높이를 덮어쓰는 대신, AG Grid가 처음 행 높이를 계산하는 단계부터 세로 여백을 반영하도록 바꾼 것이 핵심이었다.
이를 적용한 뒤에는 로딩이 끝난 후 데이터가 표시될 때 발생하던 높이 점프가 사라졌다. 별도의 재계산 과정 없이 행이 처음부터 의도한 높이로 렌더링되는 것도 확인할 수 있었다.
단, rowHeight를 고정값으로 설정한 경우에는 rowVerticalPaddingScale이 적용되지 않으므로 두 옵션을 함께 사용할 때는 주의해야 한다.
AG Grid의 내장 오버레이는 표시할 컴포넌트를 변경하는 기능을 제공하지만, 이번 프로젝트에서 필요했던 높이와 여백까지 세밀하게 조정하기에는 한계가 있었다.
따라서 로딩 상태는 내장 오버레이를 끄고 별도의 컴포넌트를 그리드 위에 직접 표시했다. 빈 데이터와 에러 상태는 No Rows Overlay 영역을 재사용하되, 상태별로 프로젝트 공통 컴포넌트를 연결했다.
공통 컴포넌트를 재사용해 다른 화면과의 일관성을 유지하면서도, 각 오버레이의 높이와 배치는 직접 제어할 수 있도록 구현했다.
추가 요구사항을 처리하는 과정에서는 그리드와 사용처에서 상태를 각각 판별하는 것이 중복인지 고민했다. 최종적으로는 공통 그리드가 자신의 데이터 영역을 책임지고, 사용처가 버튼을 포함한 페이지 레이아웃을 책임지도록 경계를 나눴다.
또한 autoHeight의 첫 렌더링에서 나타난 높이 점프는 SCSS 패딩을 테마 파라미터인 rowVerticalPaddingScale로 옮겨 해결했다. 렌더링이 완료된 뒤 스타일로 덮어쓰는 대신, AG Grid가 행 높이를 계산하는 단계에서부터 필요한 여백을 반영하도록 변경한 것이 차이점이었다.
눈에 보이는 시간은 짧았지만, 로딩 이후 발생하는 작은 높이 변화도 전체 사용성을 어색하게 만들 수 있었다. 이번 작업을 통해 화면에 표시되는 결과뿐 아니라, 상태가 전환되는 과정까지 함께 확인하고 다듬는 것이 중요하다는 점을 배웠다.