출처: https://www.sonarsource.com/blog/es2023-new-array-copying-methods-javascript/
ECMAScript 2023 사양이 최근 확정되었습니다. 여기에는 자바스크립트 프로그램을 더욱 예측 가능하고 유지보수하기 쉽게 만드는 데 도움이 되는 배열 객체에 대한 몇 가지 새로운 메서드가 포함되었습니다. toSorted
, toReversed
, toSpliced
및 with
메서드를 사용하면, 기존 배열의 데이터를 변경하지 않고 복사본을 만든 뒤 복사본을 변경하여 배열에 대한 연산을 수행합니다. 이 메서드들의 차이점과 프로젝트에서 어떻게 사용할 수 있을지 알아보고 싶으시다면 아래 내용을 계속 읽어보세요.
배열 객체에는 항상 몇 가지 특이한 점이 있었습니다. sort
, reverse
그리고 splice
와 같은 메서드는 원본 배열을 변경합니다. 반면 concat
, map
, filter
와 같은 메서드는 배열의 복사본을 만든 다음 복사본에 대해 연산합니다. 객체 자체를 변경시키는 연산을 수행하면 사이드 이펙트가 발생하여 시스템 내에서 예기치 않은 동작이 발생할 수 있습니다.
예를 들어 배열의 순서를 거꾸로 뒤집을 때 아래와 같은 일이 발생할 수 있습니다.
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const reversed = languages.reverse();
console.log(reversed);
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
console.log(languages);
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
console.log(Object.is(languages, reversed));
// => true
원래 배열이 거꾸로 뒤집어졌으며, 배열을 뒤집은 결과를 새 변수에 할당했지만 두 변수 모두 동일한 배열을 가리키고 있는 것을 볼 수 있습니다.
원본 배열을 변경하는 메서드를 사용했을 때 발생할 수 있는 가장 잘 알려진 문제 중 하나는 리액트 컴포넌트에서 배열을 사용할 때입니다. 배열 자체가 동일한 객체이기 때문에, 배열을 변경한 후 새로운 상태로 설정하려고 해도 새 렌더링이 발생하지 않습니다. 대신 배열을 먼저 복사한 다음 복사본을 변경하고 이를 새 상태로 설정해야 합니다. 리액트 문서에는 이 때문에 배열의 상태를 업데이트하는 방법을 설명하는 페이지도 존재합니다.
이 문제를 해결하는 방법은 배열을 먼저 복사한 다음 변경하는 것입니다. 배열의 복사본을 만드는 방법에는 Array.from
, 스프레드 연산자(...
), 인수가 없는 slice
함수 호출 등 여러 가지 방법이 있습니다.
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const reversed = Array.from(languages).reverse();
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
console.log(languages);
// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]
console.log(Object.is(languages, reversed));
// => false
해결 방법이 있다는 것은 다행이지만, 상태 변경 전 복사를 먼저 수행하는 것을 기억해야 한다는 점은 좋지 않습니다.
이것이 바로 새로운 메서드들이 등장한 이유입니다. toSorted
, toReversed
, toSpliced
그리고 with
메서드는 원본 배열을 복사하고 복사본을 변경한 후 반환합니다. 하나의 함수만 호출하면 되기 때문에 코드를 작성하는 데에도 더 쉬워지고 배열을 먼저 복사할 필요가 없으므로 읽기도 더 쉬워집니다. 그렇다면 각 메서드는 무엇을 수행할까요?
Array.prototype.toSorted
toSorted
함수는 새롭게 정렬된 배열을 반환합니다.
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const sorted = languages.toSorted();
console.log(sorted);
// => [ 'CoffeeScript', 'JavaScript', 'TypeScript' ]
console.log(languages);
// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]
sort
함수에는 복사하는 것 외에 예상치 못한 동작이 몇 가지 있는데, toSorted
함수도 그 동작을 공유합니다. 악센트 기호를 포함한 문자열이나 숫자를 정렬하는 경우 여전히 주의해야합니다. 원하는 결과를 생성하는 비교 함수(String
의 localeCompare
와 같은)를 작성해야 합니다.
const numbers = [5, 3, 10, 7, 1];
const sorted = numbers.toSorted();
console.log(sorted);
// => [ 1, 10, 3, 5, 7 ]
const sortedCorrectly = numbers.toSorted((a, b) => a - b);
console.log(sortedCorrectly);
// => [ 1, 3, 5, 7, 10 ]
const strings = ["abc", "äbc", "def"];
const sorted = strings.toSorted();
console.log(sorted);
// => [ 'abc', 'def', 'äbc' ]
const sortedCorrectly = strings.toSorted((a, b) => a.localeCompare(b));
console.log(sortedCorrectly);
// => [ 'abc', 'äbc', 'def' ]
Array.prototype.toReversed
toReversed
함수를 사용하면 순서가 반전된 새 배열이 반환됩니다.
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const reversed = languages.toReversed();
console.log(reversed);
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
Sonar에는 reverse
와 같이 오류의 소지가 있는 메서드를 다루는 규칙이 있습니다. reverse
의 결과를 새 변수에 할당하는 것은 원래의 배열도 변경되었기 때문에 오류가 발생할 가능성이 있습니다. 이제 배열을 복사하고 복사본을 변경하는 대신 toReversed
또는 toSorted
를 사용할 수 있습니다.
Array.prototype.toSpliced
toSpliced
함수는 기존의 splice
와 조금 다릅니다. splice
는 제공된 인덱스 값에 요소를 삭제하고 추가하며 기존 배열을 변경하고 삭제된 요소를 포함하는 배열을 반환하지만, toSpliced
함수는 제거된 요소 없이 추가된 요소만 포함하여 새로운 배열을 반환합니다. 작동 방식은 다음과 같습니다.
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const spliced = languages.toSpliced(2, 1, "Dart", "WebAssembly");
console.log(spliced);
// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]
반환 값에 splice
를 사용하는 경우, toSpliced
는 대체되지 않습니다. 기존 배열을 변경하지 않고 삭제된 요소도 알고 싶다면 복사 메서드인 slice
를 사용해야 합니다.
안타깝게도 splice
와 slice
는 필요한 인수가 다릅니다. splice
는 인덱스와 해당 인덱스 뒤의 제거한 요소의 수를, slice
는 시작과 끝의 두 인덱스를 인수로 받습니다. splice
대신 toSpliced
를 사용하면서 삭제된 요소도 가져오고 싶다면 다음과 같이 원래 배열에 toSpliced
와 slice
를 적용하면 됩니다.
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const startDeletingAt = 2;
const deleteCount = 1;
const spliced = languages.toSpliced(
startDeletingAt,
deleteCount,
"Dart",
"WebAssembly"
);
const removed = languages.slice(startDeletingAt, startDeletingAt + deleteCount);
console.log(spliced);
// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]
console.log(removed);
// => [ 'CoffeeScript' ]
Array.prototype.with
with
함수는 배열의 한 요소를 변경하기 위해 대괄호 표기법을 사용하는 것과 같은 복사 기능입니다. 따라서 아래와 같이 배열을 직접 변경하는 대신,
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
languages[2] = "WebAssembly";
console.log(languages);
// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]
배열을 복사하고 요소를 변경할 수 있습니다.
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const updated = languages.with(2, "WebAssembly");
console.log(updated);
// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]
console.log(languages);
// => [ 'JavaScript', 'TypeScript', CoffeeScript' ]
일반 배열 객체만이 새로운 메서드의 이점을 누릴 수 있는 것은 아닙니다. 모든 TypedArray
에 toSorted
, toReversed
, with
를 사용할 수 있습니다. Int8Array
부터 BigUint64Array
까지 모든 TypedArray
에 적용됩니다. TypedArray
에는 splice
메서드가 없으므로 toSpliced
메서드 또한 사용할 수 없습니다.
map
, filter
, concat
와 같은 메서드는 이미 복사를 수행한다고 이야기 했습니다. 하지만 이러한 메서드와 새로운 복사 메서드 사이에는 차이가 있습니다. 내장된 Array
객체를 확장한 인스턴스에서 map
, flatMap
, filter
혹은 concat
를 사용하면 동일한 타입의 새 인스턴스가 반환됩니다. Array
를 확장하고 toSorted
, toReversed
, toSpliced
혹은 with
를 사용하면 일반 Array
를 반환합니다.
class MyArray extends Array {}
const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");
const upcase = languages.map((language) => language.toUpperCase());
console.log(upcase instanceof MyArray);
// => true
const reversed = languages.toReversed();
console.log(reversed instanceof MyArray);
// => false
MyArray.from
을 사용하여 다시 커스텀 Array
로 되돌릴 수 있습니다.
class MyArray extends Array {}
const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");
const reversed = MyArray.from(languages.toReversed());
console.log(reversed instance of MyArray);
// => true
ECMAScript 2023 스펙은 아주 최근의 것이지만 새로운 배열 메서드에 대한 지원은 이미 잘 이루어지고 있습니다. Chrome 110, Safari 16.3, Node.js 20, Deno 1.31 은 네 가지 메서드 모두 지원하며 아직 지원하지 않는 플랫폼을 위한 폴리필과 shim도 존재합니다.
예측 가능한 코드를 더 쉽게 작성할 수 있도록 ECMAScript 표준에 이와 같은 기능이 추가되었다는 것은 매우 반가운 일입니다. ES2023에 포함된 다른 몇 가지 제안도 있으니 관심 있으시다면 확인해 보세요. 이 스펙에 포함될 예정인 다른 제안서도 확인해보고 싶다면 TC39 제안 레포지토리를 확인해보세요.
함수명이 영 맘에 들진 않지만 쏠쏠한 것들이 추가되었군요! 번역 감사합니다. 잘 읽었어요~!