본문 바로가기
빌드노트 검색

빌드노트

splice 반환값을 저장했더니 지운 항목만 남는다

splice의 반환값은 삭제 목록이고 편집 후 전체 목록은 바뀐 원본에 남는다. 같은 삭제·삽입 입력으로 toSpliced의 새 반환 배열과 비교하고, 지원 확인과 복사 후 splice 대안, 중첩 객체 참조가 공유되는 범위를 실제 실행으로 구분한다.

JavaScript 목록에서 const next = items.splice(index, 1)로 항목 하나를 지운 뒤 next를 화면에 전달하면, 남길 목록 대신 지운 항목만 전달하게 된다. splice()는 원본 배열을 바꾸고, 반환값에는 삭제한 항목을 담는다. 편집 후 전체 목록을 새 배열로 받아야 한다면 지원되는 환경에서 toSpliced()를 쓴다.

먼저 목록을 저장하거나 전달하는 줄에서 splice()의 반환값을 쓰는지 확인한다. items = items.splice(...)도 같은 문제다. 변수 items가 삭제 목록을 가리키도록 바뀌며, 호출 전 배열을 다른 곳에서도 참조했다면 그 배열은 이미 편집되어 있다. 반환값을 버리는 것만으로 원본 변경까지 취소되지는 않는다.

같은 자리를 바꿔도 반환하는 배열이 다르다

MDN의 splice 설명 (새 탭에서 열림)은 반환값을 삭제된 요소의 배열로 정의한다. toSpliced 설명 (새 탭에서 열림)은 원본을 수정하지 않고 편집 결과를 새 배열로 반환한다고 설명한다. 두 메서드 모두 시작 인덱스, 제거할 개수, 넣을 항목을 받지만 반환값의 용도가 다르다.

다음 코드는 B를 지우고 같은 자리에 D를 넣은 뒤, 반환값과 원본을 따로 읽는다. Node.js가 설치된 환경에서 먼저 node -p "typeof Array.prototype.toSpliced"를 실행한다. 이번 Windows의 Node.js 22.23.2에서는 function이었다. undefined라면 아래 비교의 toSpliced() 호출을 실행할 수 없으므로 다음 절의 대안을 확인한다. node 명령 자체를 찾지 못하면 Node.js 실행 환경부터 준비해야 한다.

빈 폴더에 splice-demo.mjs를 만들고 다음 내용을 저장한 뒤, 그 폴더에서 node splice-demo.mjs를 실행한다. 실제 앱의 목록은 사용하지 않는다.

const mutable = ["A", "B", "C"];
const removed = mutable.splice(1, 1, "D");
console.log("splice return", JSON.stringify(removed));
console.log("splice original", JSON.stringify(mutable));

const original = ["A", "B", "C"];
const next = original.toSpliced(1, 1, "D");
console.log("toSpliced return", JSON.stringify(next));
console.log("toSpliced original", JSON.stringify(original));

위 코드를 실행한 출력이다. 배열을 문자열로 출력해 각 호출 직후의 내용을 기록했다.

splice return ["B"]
splice original ["A","D","C"]
toSpliced return ["A","D","C"]
toSpliced original ["A","B","C"]

1은 두 번째 칸의 인덱스이고, 그다음 1은 제거할 개수다. splice()의 반환값에는 새로 넣은 D도, 유지한 A와 C도 없다. 화면에 편집 후 전체 목록을 전달하려는 코드에서 removed를 쓰면 잘못된 목록을 넘기게 된다. 이 설명은 위 배열 값에서 내린 판단이며 화면을 직접 실행한 결과는 아니다.

같은 초기 배열로 삭제만 하는 (1, 1)도 따로 실행했다. splice()는 ["B"]를 반환하고 원본은 ["A","C"]가 됐다. toSpliced()의 반환값은 ["A","C"]였고 원본은 유지됐다. 삽입만 하는 (1, 0, "D")에서는 splice()의 반환값이 []였다. 삭제가 없으니 빈 배열이 정상 반환값이다. 이때도 splice()로 편집한 원본과 toSpliced()의 반환값은 모두 ["A","D","B","C"]였고, toSpliced()를 호출한 원본은 ["A","B","C"]로 남았다.

새 목록이 필요하면 반환값과 지원 환경을 함께 고른다

원본 변경이 허용되는 일반 배열이라면 const removed = items.splice(...)로 지운 항목을 받고, 편집 후 목록은 items에서 읽는다. 이전 배열을 보존해야 하는 갱신이라면 const next = items.toSpliced(...)로 새 목록을 받는다. 이름을 바꾸는 것만으로는 부족하며, 어느 배열을 다음 단계에 넘기는지가 핵심이다.

지원 확인은 실제 코드를 실행할 환경에서도 해야 한다. 앞의 Node.js 확인은 배포 대상 브라우저의 지원을 보장하지 않는다. toSpliced()가 없다면 지원되는 런타임을 사용하거나 복사한 배열에 splice()를 적용할 수 있다. 다음은 복사 후에도 반환값이 아니라 편집한 복사본 next를 사용하는 예다. 별도 파일 splice-copy.mjs에 저장하고 node splice-copy.mjs로 실행할 수 있다.

const original = ["A", "B", "C"];
const next = original.slice();
next.splice(1, 1, "D");
console.log(JSON.stringify(next), JSON.stringify(original));

같은 Node.js에서 출력은 ["A","D","C"] ["A","B","C"]였다. const next = original.slice().splice(1, 1, "D")처럼 이어 쓰면 다시 삭제 목록 ["B"]를 저장하게 된다. 이 대안은 여기서 사용한 빈 슬롯 없는 일반 배열을 대상으로 하며, toSpliced()의 모든 동작을 대체하는 폴리필로 제시한 것은 아니다.

새 배열이어도 안의 객체는 공유한다

toSpliced()가 원본 배열을 편집하지 않는다는 말은 요소 객체까지 깊게 복제한다는 뜻이 아니다. MDN의 배열 복사 설명 (새 탭에서 열림)에 따르면 객체 요소는 참조가 복사된다. ECMAScript의 toSpliced 알고리즘 (새 탭에서 열림)도 원본에서 읽은 요소 값을 새 배열에 넣는다.

다음 코드는 바깥 배열과 남겨 둔 요소의 참조를 비교한 뒤, 그 요소 안의 속성을 바꿔 본다. toSpliced() 지원을 확인한 환경에서 별도 파일 splice-refs.mjs로 저장하고 node splice-refs.mjs를 실행한다.

const original = [{ meta: { selected: false } }, { id: "B" }];
const next = original.toSpliced(1, 1);
console.log(next === original, next[0] === original[0],
  next[0].meta === original[0].meta);
next[0].meta.selected = true;
console.log(original[0].meta.selected, original.length, next.length);

실제 출력은 첫 줄이 false true true, 다음 줄이 true 2 1이었다. 배열은 별개이고 원본 길이는 그대로지만, 남겨 둔 요소와 그 안의 meta는 같은 객체였다. 나중에 next[0].meta.selected를 바꾸자 원본에서 읽어도 true였다. 이전 목록의 요소 내용까지 보존해야 한다면 수정할 요소와 중첩 객체를 새로 만드는 작업이 별도로 필요하다.

별도 객체 배열의 교체 실험에서는 splice()가 반환한 삭제 요소도 호출 전에 보관한 그 요소와 같은 참조였다. toSpliced()에 삽입 인자로 넘긴 객체 역시 새 배열 안에서 같은 참조로 유지됐다. 삭제 목록을 받거나 새 배열을 받는 어느 쪽도 요소 객체의 복제를 뜻하지 않는다.

이번 확인은 Windows의 Node.js 22.23.2에서 작은 합성 배열의 반환값·원본·객체 참조를 비교한 것이다. 브라우저별 실행, 메서드가 없는 구형 런타임, 폴리필, 프레임워크의 화면 갱신은 검증하지 않았다. 자기 코드에서는 먼저 전체 목록이 필요한 자리에 삭제 목록을 전달했는지 확인하고, 이전 배열 보존이 필요할 때 실행 환경에 맞는 새 배열 생성 방법을 선택한다.

← 전체 글 보기