본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev32

sort()가 원본 배열을 바꿔서 화면이 엉킨 버그와 toSorted()

  • #Array
  • #Engineering Note
  • #JavaScript

문제 발생

목록 컴포넌트에서 정렬 버튼을 눌렀더니, 정렬은 됐는데 같은 데이터를 쓰는 다른 화면의 순서까지 함께 바뀌었습니다. 되돌릴 방법도 없었습니다.

function SortedList({ items }) {
  const sorted = items.sort((a, b) => a.price - b.price); // 원본을 바꾼다
  return <ul>{sorted.map(...)}</ul>;
}

원인 분석

Array.prototype.sort()원본 배열을 그 자리에서 정렬하고 그 배열 자체를 반환합니다. 새 배열을 주는 것처럼 생겼지만 sorted === items입니다. 그래서 items를 넘겨준 부모의 상태도 같이 정렬됩니다.

같은 성격의 메서드가 더 있습니다 — reverse(), splice(), 그리고 arr[i] = x 같은 직접 대입입니다. 이들 모두 호출한 쪽이 소유하지 않은 배열을 조용히 바꿉니다. React처럼 "상태는 불변으로 다룬다"를 전제하는 환경에서는 리렌더가 건너뛰어지는 문제까지 겹칩니다.

지금까지의 해법은 [...items].sort(...)처럼 먼저 복사하는 것이었고, 그 복사를 빠뜨리는 것이 이 버그의 정체였습니다.

ES2023의 change-array-by-copy로 복사본을 돌려주는 짝이 표준에 들어왔습니다. 2023년 7월부터 모든 최신 브라우저에서 사용 가능(Baseline)합니다.

원본을 바꾸는 메서드복사본을 돌려주는 메서드
sort()toSorted()
reverse()toReversed()
splice()toSpliced()
arr[i] = xwith(i, x)

해결 방안

  1. 정렬·역순·교체는 복사 메서드로 바꿉니다.
function SortedList({ items }) {
  const sorted = items.toSorted((a, b) => a.price - b.price); // 원본 유지
  return <ul>{sorted.map(...)}</ul>;
}
const months = ["Mar", "Jan", "Feb"];
months.toSorted();   // ['Feb', 'Jan', 'Mar']
months;              // ['Mar', 'Jan', 'Feb'] — 그대로
  1. 한 항목만 바꿀 때는 with()를 씁니다. map으로 인덱스를 비교하는 코드보다 의도가 분명합니다.
const next = items.with(index, { ...items[index], done: true });
  1. 원본을 바꿔도 되는 자리라면 sort()를 계속 씁니다. 지역 변수로 방금 만든 배열이라면 복사가 낭비입니다. 기준은 "이 배열을 내가 소유하고 있는가"입니다.
  2. 린트로 강제할 수 있습니다. 소유하지 않은 배열에 대한 변경 메서드 호출을 금지하는 규칙을 켜두면 리뷰에서 놓쳐도 걸립니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.