자바스크립트 배열에서 findRowExpr·getCaseCount 처럼 조건으로 행 찾기
투비소프트 XPLATFORM 화면을 인스웨이브 WebSquare5 로 옮기는 프로젝트를 한 적이 있다. XPLATFORM 의 Dataset 에는 조건식으로 행을 찾는 findRowExpr, 조건에 맞는 행 수를 세는 getCaseCount 가 있는데 WebSquare 의 DataList 에는 같은 것이 없었다. “이름이 ○○이 아니고 나이가 28보다 많은 첫 행” 같은 검색을 바로 할 수 없었던 것이다.
그래서 직접 만들어 썼다. WebSquare 가 아니어도 배열을 다루는 자바스크립트라면 어디서든 쓸 수 있다.
예제 데이터:
const people = [
{ 이름: "홍길동", 나이: 25, 지역: "서울" }, // 0
{ 이름: "김철수", 나이: 25, 지역: "서울" }, // 1
{ 이름: "이영희", 나이: 38, 지역: "충북" }, // 2
{ 이름: "박민수", 나이: 50, 지역: "경기" }, // 3
];
방법 1: 조건을 함수로 넘긴다 (권장)
새로 짜는 코드라면 표준 배열 메서드로 충분하다. 조건을 문자열이 아니라 함수로 넘기면 문법 오류를 편집기가 잡아 주고, eval 같은 동적 실행도 필요 없다.
// findRowExpr: 조건을 만족하는 첫 행의 인덱스, 없으면 -1
people.findIndex((row) => row.이름 === "이영희"); // 2
people.findIndex((row, i) => i !== 0 && row.지역 === "서울"); // 1
// 뒤에서부터 찾기 (ES2023)
people.findLastIndex((row) => row.지역 === "서울"); // 1
// getCaseCount: 조건을 만족하는 행 수
people.filter((row) => row.나이 > 40).length; // 1
findIndex 는 조건에 맞는 행을 찾는 즉시 멈춘다. 첫 행만 필요할 때 filter(...)[0] 처럼 전체를 도는 것보다 낫다.
방법 2: “$:컬럼” 조건식 문자열을 그대로 쓴다
기존 화면을 옮기는 중이라 "$:나이 > 40" 같은 조건식 문자열이 이미 수백 군데 있다면, 문자열을 함수로 바꿔 주는 함수를 하나 두는 편이 빠르다.
// "$:컬럼" 을 "row.컬럼" 으로 바꾸고 함수로 만든다.
// 식 안에서 row(현재 행), rowidx(행 번호)를 쓸 수 있다.
function compileExpr(expr) {
const body = expr.replaceAll("$:", "row.");
return new Function("row", "rowidx", `return (${body});`);
}
function findRowExpr(rows, expr, start = 0) {
const test = compileExpr(expr);
for (let i = start; i < rows.length; i++) {
if (test(rows[i], i)) return i;
}
return -1;
}
function getCaseCount(rows, expr) {
const test = compileExpr(expr);
return rows.reduce((count, row, i) => count + (test(row, i) ? 1 : 0), 0);
}
findRowExpr(people, "$:이름 === '이영희'"); // 2
findRowExpr(people, "$:나이 !== 25"); // 2
findRowExpr(people, "$:나이 > 40"); // 3
findRowExpr(people, "rowidx !== 0 && $:지역 === '서울'"); // 1
findRowExpr(people, "$:이름.includes('민') && $:지역 !== '충북'"); // 3
findRowExpr(people, "$:지역 === '부산'"); // -1
getCaseCount(people, "$:나이 !== 25"); // 2
getCaseCount(people, "rowidx !== 0 && $:지역 === '서울'"); // 1
주의할 점이 있다.
new Function은eval처럼 문자열을 코드로 실행한다. 조건식은 개발자가 코드에 쓴 것만 넘기고, 사용자 입력이 섞이지 않게 한다. 보안 점검에서 지적될 수 있으니 이 함수 한곳에만 두는 것이 좋다."$:"를 단순 치환하므로 조건식의 문자열 값 안에$:가 있으면 함께 바뀐다.- 찾지 못하면
-1을 돌려주게 했다. 넥사크로 Dataset 의findRowExpr도 찾지 못하면 -1 을 돌려주고,Array.prototype.findIndex와도 같아서 다루기 쉽다.
덤: WebSquare5 DataList 필터
같은 조건식으로 WebSquare5 DataList 에 필터를 거는 함수도 만들어 썼다. setColumnFilter 에 type: "func" 로 판정 함수를 넘기면, 함수가 true 를 돌려준 행만 남는다.
function applyFilter(dataList, expr) {
if (!dataList) return;
dataList.clearFilter(); // 기존 필터 해제
if (!expr) return;
const test = compileExpr(expr);
dataList.setColumnFilter({
type: "func",
colIndex: 0,
key(cellData, param, rowIndex) {
return test(dataList.getRowJSON(rowIndex), rowIndex);
},
condition: "and",
});
}
applyFilter(dl_main, "$:국가 !== '대한민국'");
화살표 함수·replaceAll 같은 문법은 프로젝트의 대상 브라우저에 맞춰 바꾼다. 구형 브라우저까지 지원해야 하면 function 표현식과 split("$:").join("row.") 로 쓰면 된다.
다시 쓰며 바로잡은 점
- 원문 구현은
filter콜백 안에서 원본 데이터의 각 행에rowidx속성을 덧붙였다. 호출할 때마다 원본이 바뀌는 부작용이 있어서, 행 번호는 인자로만 넘기도록 고쳤다. - 원문의
findRowExpr는 첫 행을 찾은 뒤에도 배열 끝까지 돌았고, 못 찾으면undefined를 돌려줬다. 찾으면 바로 멈추고 못 찾으면-1을 돌려주게 바꿨다. eval은 호출한 함수의 지역 변수까지 건드릴 수 있어서, 지역 범위를 보지 못하는new Function으로 바꿨다. 그래도 동적 코드 실행이라는 점은 같다는 것을 밝혔다. 새 코드라면 조건을 함수로 넘기는 방법 1 을 권한다.