왜 종속 드롭다운과 유효성 검사가 함께 필요할까요
엑셀처럼 보이는 입력 화면에서 ‘지역 → 도시’처럼 앞 선택이 뒤 선택을 제한해야 하는 경우가 많습니다. 하지만 목록만 연결해 두면 사용자가 순서를 바꾸거나 붙여넣기할 때 규칙이 쉽게 깨집니다.
SpreadJS에는 리스트 유효성 검사와 이벤트 훅이 있지만, 종속(카스케이딩) 드롭다운을 안정적으로 만들려면 유효성 규칙과 데이터 소스를 함께 설계해야 합니다. 그렇지 않으면 잘못된 조합이 저장되거나 숨은 시트의 참조가 어긋나는 문제가 생길 수 있습니다.
이 글은 “데이터 소스 준비 → 첫 번째 드롭다운 구성 → 두 번째 드롭다운 동기화 → 붙여넣기·지우기 예외 처리” 순서로 구현을 안내합니다. 실무 화면에 바로 적용할 수 있는 최소 코드와 주의사항을 포함합니다.
개념을 사례로 이해하기: ‘카테고리 → 상품’ 흐름
전자상거래 입력 폼을 예로 들면, 상위 셀에서 ‘카테고리’를 고르면 하위 셀 ‘상품’ 목록이 그에 맞게 바뀌어야 합니다. 종속 드롭다운은 상위 선택값을 키로 삼아 하위 목록을 동적으로 생성하고, 유효성 검사는 그 조합이 올바른지 끝까지 감시합니다.
SpreadJS에서는 데이터 소스(예: { 카테고리: [상품...] })를 메모리나 숨김 시트에 두고, 상위 셀 변경 이벤트로 하위 셀의 리스트 유효성 규칙을 갱신합니다. 붙여넣기나 삭제 같은 비정상 입력도 이벤트로 가로채어 규칙에 맞는지 다시 확인하는 구조가 안전합니다.
핵심 체크포인트는 다음과 같습니다.
- 데이터는 “키-목록” 구조로 단순하게 보관
- 상위 변경 시 하위 유효성 규칙 즉시 재설정
- 붙여넣기/지우기 후에도 규칙 재검사
- 잘못된 조합은 메시지 또는 초기화로 처리
종속 드롭다운 최소 구현과 검증 포인트
카테고리→상품 종속 드롭다운의 최소 흐름입니다. 각 단계마다 유효성·이벤트를 함께 설계하는 것이 안정적입니다.
- 데이터: {카테고리: 상품배열}을 메모리/숨김 시트에 저장
- 상위(카테고리) 드롭다운 유효성 적용
- 상위 변경 시 하위(상품) 유효성 행 단위 재설정
- 붙여넣기·지우기 후 하위 값 재검사 및 초기화/알림
- 보호/숨김, 참조 범위는 사용 시점 재확인
아래 예시는 상위 변경에 따라 하위를 갱신하고, 붙여넣기도 가로챕니다. 구조 이해용으로 축약했습니다.
// 가정: sheet는 GC.Spread.Sheets.Worksheet
const map = { "과일": ["사과","배","포도"], "음료": ["콜라","주스","커피"] };
const colCat = 0, colItem = 1;
// 1) 상위 유효성
sheet.setDataValidator(0, colCat, sheet.getRowCount(), 1,
new GC.Spread.Sheets.DataValidation.createListValidator(Object.keys(map)));
// 2) 상위 변경 → 하위 유효성 재설정(행 단위)
sheet.bind(GC.Spread.Sheets.Events.ValueChanged, (s, a) => {
if (a.col !== colCat) return;
const r = a.row, cat = s.getValue(r, colCat);
const list = Array.isArray(map[cat]) ? map[cat] : [];
s.clear(r, colItem, 1, 1, GC.Spread.Sheets.SheetArea.viewport, GC.Spread.Sheets.StorageType.data);
s.setDataValidator(r, colItem, 1, 1, GC.Spread.Sheets.DataValidation.createListValidator(list));
});
// 3) 붙여넣기/지우기 후 재검사
sheet.bind(GC.Spread.Sheets.Events.ClipboardPasted, (s, e) => {
for (let r = e.cellRange.row; r < e.cellRange.row + e.cellRange.rowCount; r++) {
const cat = s.getValue(r, colCat), item = s.getValue(r, colItem);
if (!(map[cat] || []).includes(item)) s.setValue(r, colItem, null); // 또는 경고
}
});
핵심은 하위를 행 단위로 재설정하고, 붙여넣기 이벤트에서 값까지 검증하는 것입니다. 경고/초기화 방식은 화면 정책에 맞춰 선택하세요.
종속 드롭다운 실무 체크포인트
상위 변경만 처리하고 붙여넣기·지우기를 누락하면 하위 유효성이 어긋납니다. 다중 붙여넣기 뒤 재검사가 없으면 겉으로 정상처럼 보여도 조합이 깨진 상태가 됩니다.
숨김 시트·이름 범위는 참조 밀림이 잦습니다. 소스 행 변동이 예상되면 동적 이름 범위로 관리하거나 JSON 메모리 맵으로 고정하는 편이 안전합니다.
이벤트는 용도별로 분리하세요.
- 상위 변경: ValueChanged
- 붙여넣기: ClipboardPasting/ClipboardPasted
- 지우기: EditEnded/RangeChanged
위와 같이 훅을 나눠 타이밍 충돌을 줄입니다.
선택 기준
- 소스 작고 변경 잦음: 메모리 맵 + 이벤트로 유효성 동적 재설정
- 수식 기반 범위 필요: 숨김 시트 + 이름 범위, 참조 무결성 점검
- 입력 신뢰성 최우선: 잘못된 조합 시 값 초기화 + 사용자 메시지
하위 값 강제 초기화가 필요하면 “붙여넣기 이후” 재검사를 한 번 더 두세요.
아래 코드는 구조 설명용 예시입니다.
sheet.bind(GC.Spread.Sheets.Events.ClipboardPasted, function(_, args) {
const r = args.cellRange;
for (let row = r.row; row < r.row + r.rowCount; row++) {
const cat = sheet.getValue(row, colCat);
const item = sheet.getValue(row, colItem);
const list = map[cat] || [];
if (item && list.indexOf(item) < 0) sheet.setValue(row, colItem, null);
}
});
사전 차단이 어렵다면 사후 정리 전략이 단순하고 유지보수에 유리합니다.
종속 드롭다운 빠른 적용 요약
종속 드롭다운은 소스 구조, 유효성, 이벤트 흐름을 함께 설계해야 안정적으로 동작합니다. 핵심은 상위 변경·비정상 입력 후 하위를 즉시 재검사하는 구조입니다.
바로 적용 체크리스트
- 소스: {키: 목록[]}로 메모리/숨김 시트 저장
- 상위 컬럼: 리스트 유효성, 초기값 비움
- ValueChanged: 행 기준 하위 유효성 재설정
- ClipboardPasted/RangeChanged: 하위 값 검증, 불일치 시 초기화·알림
- 소스 변동: 이름 범위·맵 동기화 함수를 분리
아래 함수는 “상위값→하위 유효성”을 일괄 갱신합니다. 컬럼 인덱스·메시지는 프로젝트에 맞게 조정하세요.
function syncDependentList(sheet, row, colParent, colChild, map) {
const cat = sheet.getValue(row, colParent);
const items = Array.isArray(map[cat]) ? map[cat] : [];
const dv = new GC.Spread.Sheets.DataValidation.createListValidator(items);
sheet.setDataValidator(row, colChild, dv);
const val = sheet.getValue(row, colChild);
if (!items.includes(val)) sheet.setValue(row, colChild, null);
}
도입 전 점검: 대량 붙여넣기 성능, 참조 범위 최신성, 사용자 알림 문구. 필요하면 저장 전 최종 검증으로 안전망을 보강하세요.
붙여넣기·삭제 안전화: 삼단계 이벤트
대량 붙여넣기·삭제는 리스트 유효성만으로 막기 어렵습니다. 핵심은 ‘붙여넣기 전 필터 → 붙여넣기 후 재검사 → 삭제 후 초기화’ 흐름입니다.
아래 코드는 상·하위 열을 고정하고 이벤트로 값과 규칙을 동기화합니다. 예시는 구조를 보여주기 위한 형태입니다.
// 가정: sheet, map, colCat=0, colItem=1
sheet.bind(GC.Spread.Sheets.Events.ClipboardPasting, (s, e) => {
// 1) 붙여넣기 전: 필요 시 하위열 잠금/마스크
if (e.copyData) return; // 잘라내기/복사 구분은 상황에 맞게
});
sheet.bind(GC.Spread.Sheets.Events.ClipboardPasted, (s, e) => {
// 2) 붙여넣기 후: 상위값 기준 하위 유효성/값 재검사
const r1 = e.cellRange.row, r2 = r1 + e.cellRange.rowCount;
for (let r = r1; r < r2; r++) {
const cat = s.getValue(r, colCat);
const list = Array.isArray(map[cat]) ? map[cat] : [];
const dv = new GC.Spread.Sheets.DataValidation.createListValidator(list);
s.setDataValidator(r, colItem, dv);
const val = s.getValue(r, colItem);
if (!list.includes(val)) s.setValue(r, colItem, null);
}
});
sheet.bind(GC.Spread.Sheets.Events.EditEnded, (s, e) => {
// 3) 삭제: 상위가 비면 하위 유효성/값 초기화
if (e.col === colCat && s.getValue(e.row, colCat) == null) {
s.setDataValidator(e.row, colItem, null);
s.setValue(e.row, colItem, null);
}
});
포인트는 행 단위 순회로 하위 유효성을 재설정하고 목록 외 값은 즉시 비우는 것입니다.
경고나 롤백은 팀 정책에 맞춰 추가하세요.
'프로그래밍 > spreadjs' 카테고리의 다른 글
| SpreadJS 계산 성능 튜닝: 재계산 모드 설정과 CalculationProgress 이벤트로 대용량 시트 최적화 (0) | 2026.06.22 |
|---|---|
| SpreadJS AI 엑셀 함수 – 스프레드시트에 AI 어시스턴트를 적용하는 방법 (0) | 2026.04.03 |
| SpreadJS AI 수식 편집 – 자연어로 엑셀 수식 만드는 방법 (0) | 2026.03.25 |
| SpreadJS AI Pivot Panel – 자연어로 피벗 테이블 만드는 방법 (0) | 2026.03.24 |
| 엑셀 JSON 데이터 저장/복원하기 - spreadjs (0) | 2026.01.12 |