단진
대체로 맑음
단진
  • 분류 전체보기 (164)
    • 개발 (114)
      • 회고 (25)
      • 개발과정 (28)
      • 개념 (15)
      • JavaScript (12)
      • TypeScript (12)
      • 알고리즘 (4)
      • GitHub (4)
      • 오류 (9)
      • TMI (5)
    • 일상 (15)
      • 사진 및 여행 (6)
      • 책 소개 (4)
      • 기타 TMI (5)
    • IT (16)
      • 개념 (5)
      • 데이터베이스 (6)
      • 딥러닝 (1)
      • TMI (4)
    • TMI (4)
      • 법률 TMI (4)
    • 보안 (15)
      • Dreamhack (5)
      • Root Me (10)
hELLO · Designed By 정상우.
단진

대체로 맑음

구조 분해 할당 (feat. ES6, 단점)
개발/개념

구조 분해 할당 (feat. ES6, 단점)

2025. 8. 11. 20:43

배열에서 두 요소를 바꾸는 경우 아래와 같이 작성하곤 한다.

const arr = [1, 2, 3, 4, 5];

[arr[1], arr[2]] = [arr[2], arr[1]]

console.log(arr); // [1, 3, 2, 4, 5]

위와 같이 작성할 수도 있지만 아래와 같이 작성할 수도 있다.

const arr = [1, 2, 3, 4, 5];

const tmp = arr[1];
arr[1] = arr[2];
arr[2] = tmp;

console.log(arr); // [1, 3, 2, 4, 5]

 

 

구조 분해 할당

const [state, setState] = useState();

console.log(state);


리액트에서 많이 사용하는 useState의 사용 예제는 위와 같다.

const tmp = useState();

console.log(tmp[0])


이렇게 사용하는 것과 차이가 없다.

그런데 배열의 길이가 길어지게 되고 그 배열 속에서 특정 위치에 있는 값을 변수에 할당하려 한다면 많은 코드가 필요하다는 문제가 있다.

const arr = [1, 2, 3, 4, ..., 1000000];

const first = arr[0];
const second = arr[1];
...
const million = arr[999999];


ES6에 도입된 구조분해 할당을 사용한다면 위의 코드를 아래처럼 변경할 수 있다.

const arr = [1, 2, 3, 4, ..., 1000000]; 
const [first, second, ..., million] = arr;

위와 같이 구조 분해 할당 구문은 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식이다.

 

구조 분해 할당이 가능한 이유

위의 문법은 다들 알고 있다.

하지만 저 기능이 ES6에서 사용 가능한 이유는 무엇이며 단순히 새로운 문법이라고 생각해도 되는 것일까?

결론부터 알아보면 이는 이터레이터를 활용하고 있기 때문이다.

배열의 경우 배열/이터러블 구조분해에 해당한다. (객체의 경우는 구조분해에 이터레이터를 쓰지 않는다)

이터레이터를 사용하고 있기에 이터레이터가 도입된 ES6 버전 이후에서 사용 가능한 것이다.

또한, 구조 분해 할당을 하기 위해서는 좌변에서 구조 분해 할당이 적용되어 있다는 것을 JS 엔진이 판단해야 하는데 이 부분이 ES6 이후에 도입되었다.
(객체 구조 분해 할당도 ES6 이후에 지원되는 이유)


배열의 구조 분해 할당에서 이터레이터를 활용하는 것은 아래와 같은 코드에서 확인할 수 있다.

const [a, b] = 1;

 

실제로 위와 같은 코드를 실행한다면 에러 문구는 Uncaught TypeError: 1 is not iterable로 출력된다.

 


구조 분해 할당은 좌변에 있는 타입에 따라 요구하는 데이터가 달라진다.

좌변이 배열인 경우 우변은 이터러블, 좌변이 객체인 경우 우변이 객체로 변환 가능한 값이여야 한다.

이는 좌변에 올 수 있는 것은 배열/객체 패턴이라는 의미이며 내부의 타겟은 변수, obj.prop, arr[i]와 같이 할당 가능한 위치가 올 수 있다는 것이다.

이터러블의 예시로는 Set, 문자열, Map 등이 있다.

const [a, b, c] = 'abc'; // 가능
const [a, b, c] = new Set([1, 2, 3]); // 가능

const test = {};
[test.a, test.b] = '으악';
console.log(test.a); // '으'

const { test1, test2 } = { test1: 123, test2: 456 };
const { length } = 'abc'; // 3
const { x } = null; // TypeError
const { 0: first } = [10, 20]; // 가능. first = 10

 

 

 

배열 구조 분해 할당 과정

const arr = [1, 2, 3];
const [test1, test2, test3] = arr;

 

위의 코드를 본다면 결과는 예상하듯이 test1에 1이 할당될 것이다.

이를 풀어서 다시 써보면

  • const [test1, test2, test3] = arr;
    • 구조 분해 선언문에 해당
    • 현재 렉시컬 환경에 test1, test2, test3 바인딩을 만들되 아직 초기화는 되지 않은 상태
    • 배열이므로 이터레이터 기반의 구조 분해라는 것을 판단
  • 우변 평가
    • arr 평가를 통해 1번 줄에서 만든 배열의 객체 참조를 획득
  • 이터레이터 획득
    • 배열은 이터러블이므로 arr[Symbol.iterator]()를 호출하여 ArrayIterator 생성
    • 이 이터레이터는 내부적으로 인덱스 순서로 값을 제공
  • 좌변에 할당 과정
    • 첫 번째 변수 - test1
      • iter.next() = { value: 1, done: false }
      • InitializeBinding(test1, 1) → test1 = 1
    • 두 번째 변수 - test2
      • iter.next() = { value: 2, done: false }
      • InitializeBinding(test2, 2) → test2 = 2
    • 세 번째 변수 - test3
      • iter.next() = { value: 3, done: false }
      • InitializeBinding(test3, 3) → test3 = 3
    • 네 번째
      • { value: undefined, done: true } (요청 시)

 

 

객체 구조 분해 할당 과정

const obj = { test1: 1, test2: 2, test3: 3 };
const { test1, test2, test3 } = obj;

 

 

  • const { test1, test2, test3 } = obj;
    • 구조 분해 선언문에 해당
    • 현재 렉시컬 환경에 test1, test2, test3 바인딩을 만들되 아직 초기화는 되지 않은 상태
    • 객체이므로 객체 구조 분해라는 것을 판단
  • 우변 평가
    • obj를 평가하여 1번줄에서 선언한 객체의 참조를 획득
    • ToObject 수행 (null / undefined면 TypeError)
  • 좌변 key 처리 (좌 → 우)
    • 첫 번째 key - test1
      • Get(obj, "test1") → 1
      • InitializeBinding(test1, 1) → test1 = 1
    • 두 번째 key - test2
      • Get(obj, "test2") → 2
      • InitializeBinding(test2, 2) → test2 = 2
    • 세 번째 key - test3
      • Get(obj, "test3") → 3
      • InitializeBinding(test3, 3) → test3 = 3

만약 기본값이 설정되는 경우 값이 undefined인 경우만 기본값이 할당된다.

 

null인 경우는 기본값이 적용되지 않는다.



위 과정은 JS 엔진이 담당하고 있으며 자세한 스펙은 아래 문서에서 확인할 수 있다.
https://tc39.es/ecma262/#sec-runtime-semantics-destructuringassignmentevaluation

결론적으로 “구조분해가 적용되었으니 이터레이터를 가져와서 순차적으로 할당 하거나 프로퍼티에 접근해서 할당”이라는 판단은 JS 엔진이 한다는 것이다.

따라서 개발자는 이 문법을 사용할 때 이터레이터를 가져와서 할당하는 등의 작업을 할 필요가 없는 것이다.

 

 

장점만 있는가?

벤치마크 결과

benchmark.js를 사용한 벤치마크 결과는 다음과 같다.

import Benchmark from "benchmark";

let sink = 0;

function arraySuite(n) {
  const arr = Array.from({ length: n }, (_, i) => i + 1);

  return new Benchmark.Suite(`array_${n}`)
    .add("indexing", function () {
      if (n === 5) {
        const a = arr[0],
          b = arr[1],
          c = arr[2],
          d = arr[3],
          e = arr[4];
        sink += a + b + c + d + e;
      } else if (n === 3) {
        const a = arr[0],
          b = arr[1],
          c = arr[2];
        sink += a + b + c;
      }
    })
    .add("destructure", function () {
      if (n === 5) {
        const [a, b, c, d, e] = arr;
        sink += a + b + c + d + e;
      } else if (n === 3) {
        const [a, b, c] = arr;
        sink += a + b + c;
      }
    })
    .on("cycle", function (event) {
      console.log(String(event.target));
    })
    .on("complete", function () {
      console.log(`array_${n} fastest → ${this.filter("fastest").map("name")}`);
      console.log("sink:", sink);
      sink = 0;
    });
}

function objectSuite(n) {
  const obj = {};
  for (let i = 0; i < n; i++) obj[String.fromCharCode(97 + i)] = i + 1;

  return new Benchmark.Suite(`object_${n}`)
    .add("dot access", function () {
      if (n === 5) {
        const a = obj.a,
          b = obj.b,
          c = obj.c,
          d = obj.d,
          e = obj.e;
        sink += a + b + c + d + e;
      } else if (n === 3) {
        const a = obj.a,
          b = obj.b,
          c = obj.c;
        sink += a + b + c;
      }
    })
    .add("destructure", function () {
      if (n === 5) {
        const { a, b, c, d, e } = obj;
        sink += a + b + c + d + e;
      } else if (n === 3) {
        const { a, b, c } = obj;
        sink += a + b + c;
      }
    })
    .on("cycle", function (event) {
      console.log(String(event.target));
    })
    .on("complete", function () {
      console.log(
        `object_${n} fastest → ${this.filter("fastest").map("name")}`
      );
      console.log("sink:", sink);
      sink = 0;
    });
}

function runAll() {
  [arraySuite(5), arraySuite(3), objectSuite(5), objectSuite(3)].forEach(
    (suite, i) => {
      global.gc?.();
      suite.run({ async: false });
      if (i !== 3) console.log("---");
    }
  );
}

runAll();

 

indexing x 169,216,580 ops/sec ±1.35% (92 runs sampled)
destructure x 115,857,537 ops/sec ±0.32% (99 runs sampled)
array_5 fastest → indexing
sink: 22583285190
---
indexing x 171,998,591 ops/sec ±0.86% (93 runs sampled)
destructure x 132,465,584 ops/sec ±1.79% (97 runs sampled)
array_3 fastest → indexing
sink: 9928435962
---
dot access x 170,665,770 ops/sec ±1.08% (93 runs sampled)
destructure x 169,491,900 ops/sec ±1.53% (95 runs sampled)
object_5 fastest → dot access
sink: 26993263200
---
dot access x 175,556,332 ops/sec ±0.82% (90 runs sampled)
destructure x 169,800,189 ops/sec ±0.88% (98 runs sampled)
object_3 fastest → dot access
sink: 11177306760

 

(결과에서 sink의 경우 최적화 방지용으로 출력하고 있을 뿐 성능 등의 의미있는 값이 아님)

 

 

결과 해석

  • ops/sec: 초당 수행 횟수(클수록 빠름)
  • ±%: 상대 오차(RME). 차이가 이 범위 수준이면 “유의미하지 않을 수 있음”
  • 배열
    • 길이 5: indexing 169.2M vs destructure 115.9M → indexing가 약 46% 빠름
    • 길이 3: indexing 172.0M vs destructure 132.5M → indexing가 약 30% 빠름
    • 해석: 배열에서는 구조분해가 반복마다 패턴 바인딩/이터레이터(엔진 내부 처리) 등 오버헤드가 커서 인덱스 접근이 확실히 우세. 구조분해는 바인딩 개수가 줄면 비용도 함께 줄어드는 경향이 보임.
  • 객체
    • 5개: dot 170.7M vs destructure 169.5M → dot가 약 0.7% 빠름(±1.1~1.5% 오차 범위와 비슷해 사실상 비슷)
    • 3개: dot 175.6M vs destructure 169.8M → dot가 약 3.4% 빠름(유의미하나 격차는 작음)
    • 해석: 일반적으로 직접 접근이 미세하게 유리. 다만 차이가 작아 가독성 위주로 선택해도 큰 문제 없음.

(벤치마크를 여러 번 수행하면 오차 발생 가능)

 

구조 분해 할당의 경우 간결하게 표현할 수 있다는 장점이 있으나 좌변의 패턴 파악, 우변의 평가 등 (약간의) 성능 오버헤드가 있을 수 있으며 rest를 사용하는 경우 O(n)의 추가 메모리와 시간이 필요하기 때문에 GC에 영향을 줄 수 있다. 또한, 좌변에 따라 우변에 올 수 있는 타입이 제대로 전달되었는지 신경써야 하며 구버전에 호환되지 않는다는 문제점이 있다.

 

 

그럼에도 불구하고

그럼에도 불구하고 배열 구조분해를 사용하는 이유는 가독성이 좋으며 반복적인 코드를 제거할 수 있다는 장점이 있기 때문이다.

또한 rest를 통해 나머지 값을 한 번에 가져올 수 있다는 장점이 있으며 함수의 파라미터에서 필요한 필드만 받고 기본 값까지 한 번에 선언할 수 있다는 장점이 있다.

위에서 기존의 방식보다 오버헤드가 발생할 수 있다고 했으나 요즘은 엔진 차원의 최적화가 이루어지기도 하며 저 정도의 성능을 개선하는 것 보다는 다른 성능 문제를 해결하는게 먼저일 것이다.

물론 마이크로 최적화를 하고 싶다면 위의 방법이 도움이 될 수 있으나 대부분의 경우는 이를 고려하지 않아도 아무런 문제가 없을 것이다.

 

 

결론

구조 분해 할당은 ES6에서 새롭게 도입된 문법이며 좌변에 따라 우변의 이터레이터를 활용하거나 객체로 변환하여 값에 접근 및 좌변에 할당한다.

이 과정은 엔진에 의해 수행되기 때문에 자세한 구조를 몰라도 사용하는데 문제가 없다.

성능 등의 단점이 없지는 않으나 가독성과 같은 장점도 있기 때문에 자신이 속한 집단의 컨벤션에 따르면 될 것이다.

일반적으로 권장되는 경우는 소수의 값 추출, 옵션 기본값 설정, API 응답 등에서 일부만 사용, 파라미터 선언, 간단한 스왑 등이 있으며 지양되는 경우는 성능에 민감한 루프, 대용량 전개 또는 복사, 복잡한 중첩 패턴 남용 등이 있기 때문에 성능 최적화가 최우선인 구간이 아니라면 명료성과 유지보수성 측면에서 유용하다.

ES6 이하의 버전에서 사용하기 위해서는 이 새로운 문법을 파싱하기 위한 트랜스파일 과정이 추가로 필요하다.


[참고문서]


https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Destructuring 
https://tc39.es/ecma262/#sec-runtime-semantics-destructuringassignmentevaluation 
https://tc39.es/ecma262/#sec-destructuring-assignment

 

저작자표시 비영리 변경금지 (새창열림)

'개발 > 개념' 카테고리의 다른 글

모든 브라우저는 gzip을 자동으로 해제할 수 있는가?  (0) 2025.04.09
[Node.js] Node.js와 V8 (with. ECMAScript)  (0) 2024.10.12
추상 구문 트리 (AST)란?  (0) 2024.06.13
선언형 프로그래밍과 명령형 프로그래밍 (feat. React 18 Concurrent Mode)  (0) 2024.05.04
WebRTC란 무엇이며 어떤 과정을 갖는가?  (0) 2023.11.12
    '개발/개념' 카테고리의 다른 글
    • 모든 브라우저는 gzip을 자동으로 해제할 수 있는가?
    • [Node.js] Node.js와 V8 (with. ECMAScript)
    • 추상 구문 트리 (AST)란?
    • 선언형 프로그래밍과 명령형 프로그래밍 (feat. React 18 Concurrent Mode)
    단진
    단진

    티스토리툴바