[번역] ES2023에서 도입된 자바스크립트의 새로운 배열 복사 메서드

eunbinn·2023년 5월 22일
108

FrontEnd 번역

목록 보기
20/39
post-thumbnail

출처: https://www.sonarsource.com/blog/es2023-new-array-copying-methods-javascript/

ECMAScript 2023 사양이 최근 확정되었습니다. 여기에는 자바스크립트 프로그램을 더욱 예측 가능하고 유지보수하기 쉽게 만드는 데 도움이 되는 배열 객체에 대한 몇 가지 새로운 메서드가 포함되었습니다. toSorted, toReversed, toSplicedwith 메서드를 사용하면, 기존 배열의 데이터를 변경하지 않고 복사본을 만든 뒤 복사본을 변경하여 배열에 대한 연산을 수행합니다. 이 메서드들의 차이점과 프로젝트에서 어떻게 사용할 수 있을지 알아보고 싶으시다면 아래 내용을 계속 읽어보세요.

변경과 사이드 이펙트

배열 객체에는 항상 몇 가지 특이한 점이 있었습니다. 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 함수도 그 동작을 공유합니다. 악센트 기호를 포함한 문자열이나 숫자를 정렬하는 경우 여전히 주의해야합니다. 원하는 결과를 생성하는 비교 함수(StringlocaleCompare와 같은)를 작성해야 합니다.

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를 사용해야 합니다.
안타깝게도 spliceslice는 필요한 인수가 다릅니다. splice는 인덱스와 해당 인덱스 뒤의 제거한 요소의 수를, slice는 시작과 끝의 두 인덱스를 인수로 받습니다. splice 대신 toSpliced를 사용하면서 삭제된 요소도 가져오고 싶다면 다음과 같이 원래 배열에 toSplicedslice를 적용하면 됩니다.

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' ]

배열뿐만이 아닙니다

일반 배열 객체만이 새로운 메서드의 이점을 누릴 수 있는 것은 아닙니다. 모든 TypedArraytoSorted, 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 제안 레포지토리를 확인해보세요.

1개의 댓글

comment-user-thumbnail
2023년 5월 27일

함수명이 영 맘에 들진 않지만 쏠쏠한 것들이 추가되었군요! 번역 감사합니다. 잘 읽었어요~!

답글 달기