본문 바로가기
Modern React Deep Dive/01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.6.1 구조 분해 할당

by Whiimsy 2024. 5. 14.

일반적인 자바스크립트나 Node.js 기반으로 한 코드와 리액트 코드를 비교하면 리액트 코드가 상대적으로 독특한 모습을 띰. 이러한 독특함은 JSX 구문 내부에서 객체를 조작하거나 객체의 얕은 동등 비교 문제를 피하기 위해 객체 분해 할당을 하는 등 리액트의 몇 가지 독특한 특징에서 비롯됨.

 

또한 자바스크립트는 매년 새로운 버전과 함께 새로운 기능이 나옴. 이러한 자바스크립트 표준을 ECMAScript라고 하는데, 작성하고자 하는 자바스크립트 문법이 어느 ECMAScript 버전에서 만들어졌는지도 파악해야 함. 왜냐하면 모든 브라우저와 자바스크립트 런타임이 항상 새로운 자바스크립트 문법을 지원하는 것이 아니기 때문. 예를 들어, 인터넷 익스플로러 11은 ECMAScript 5(ES5)까지만 지원하기 때문에 최신 자바스크립트 문법을 사용할 수 없음. 만약 서비스하는 웹페이지가 인터넷 익스플로러 11도 지원해야 한다면 코드에서 최신 문법을 제공할 수 없다는 점 고려해야 함. 그리고 웹페이지에 접근하는 사용자의 브라우저와 버전은 개발자와 다르게 항상 최신 버전이 아니고, 크롬, 사파리, 파이어폭스 등 다양하기 때문에 이러한 다양한 브라우저에서의 문법 지원 또한 염두에 두어야 함.

 

이러한 사용자의 다양한 브라우저 환경, 그리고 최신 문법을 작성하고 싶은 개발자의 요구를 해결하기 위해 탄생한 것이 바로 바벨. 바벨은 자바스크립트의 최신 문법을 다양한 브라우저에서도 일관적으로 지원할 수 있도록 코드를 트랜스파일함. 바벨이 어떻게 최신 코드를 트랜스파일하는지, 그리고 그 결과 어떤 코드가 생성되는지 이해하면 향후 애플리케이션을 디버깅하는 데 도움이 됨.

 

앞으로 설명할 바벨 트랜스파일 코드는 모두 ES5를 기준으로 작성됨. 인터넷 익스플로러 11의 지원은 종료됐지만 여전히 어느 정도 사용 중이기도 하고, 호환 모드를 활성화하면 엣지 환경에서도 인터넷 익스플로러 환경으로 페이지에 접근할 수 있기 때문. 이뿐만아니라 셋톱박스와 같이 업데이트를 쉽게 할 수 없는 구형 기기에서도 ES5만 작동하는 경우가 있으므로 ES5 기준으로 트랜스파일된 코드를 파악하는 것이 중요.

1.6.1 구조 분해 할당

구조 분해 할당(Destructuring assignment)이란 배열 도는 객체의 값을 말 그대로 분해해 개별 변수에 즉시 할당하는 것 의미. 배열과 객체에서 사용하며, 주로 어떠한 객체나 배열에서 선언문 없이 즉시 분해해 변수를 선언하고 할당하고 싶을 때 사용. 배열의 구조 분해 할당은 ES6(eS2015)에 처음 선보였으나, 객체 구조 분해 할당은 ECMA 2018에 와서야 처음 등장.

배열 구조 분해 할당

const array = [1, 2, 3, 4, 5]

const [first, second, third, ...arrayRest] = array
// first 1
// second 2
// third 3
// arrayRest [4, 5]

 

useState 함수는 2개 짜리 배열을 반환하는 함수이며, 첫 번째 값을 value로, 두 번째 값을 setter로 사용 가능. useState가 객체가 아닌 배열을 반환하는 이유는 객체 구조 분해 할당은 사용하는 쪽에서 원하는 이름으로 변경하는 것이 번거로움. 반대로 배열 구조 분해 할당은 자유롭게 이름을 선언할 수 있기 때문에 useState는 배열을 반환하는 것으로 추측할 수 있음.

배열 구조 분해 할당은 ,의 위치에 따라 값이 결정됨. 따라서 앞의 예제에서 중간 인덱스에 대한 할당을 생략하고 싶다면 다음과 같이 선언할 수 있음.

const array = [1, 2, 3, 4, 5]
const [first, , , , fifth] = array

 

이러한 방법은 실수를 유발할 가능성이 커서 일반적으로 배열의 길이가 작을 때 주로 쓰임. 배열 분해 할당에는 기본값을 선언할 수도 있음. 만약 사용하고자 하는 배열의 길이가 짧거나 값이 없는 경우(undefined)에는 기본 값을 사용할 것.

const array = [1, 2]
const [a = 10, b = 10, c = 10] = array
// a 1
// b 2
// c 10

 

주의할 것은 반드시 undefined일 때만 기본값을 사용한다는 것.

const [a = 1, b = 1, c = 1, d = 1, e = 1] = [undefined, null, 0, '']
// a 1
// b null
// c 0
// d ''
// e 1

 

위 배열 구조 분해 할당에서 기본값을 사용하는 것은 a와 e뿐.

 

특정값 이후의 값을 다시금 배열로 선언하고 싶다면 전개 연산자(spread operator)인 ...을 사용할 수도 있음

const array = [1, 2, 3, 4, 5]
const [first, ...rest] = array

// first 1
// rest [2, 3, 4, 5]

 

이러한 배열 구조 분해 할당 코드가 바벨에서 어떻게 트랜스파일되는지 보자.

// before
const array = [1, 2, 3, 4, 5]
const [first, second, third, ...arrayRest] = array

// after
var array = [1, 2, 3, 4, 5]
var first = array[0],
    second = array[1],
    third = array[2],
    arrayRest = array.slice(3),

 

구조 분해 할당은 단순히 배열에서 꺼내오거나 slice해서 값을 할당하는 것 볼 수 있음. 배열 구조 분해 할당 이전에는 선언을 4줄에 걸쳐서 했지만 구조 분해 할당 덕분에 이러한 작업을 단 한 줄로 처리할 수 있게 됨.

객체 구조 분해 할당

말 그대로 객체에서 값을 꺼내온 뒤 할당하는 것 의미. 배열 구조 분해 할당와 달리, 객체는 객체 내부 이름으로 꺼내옴.

const object = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1,
}

const { a, b, c, ...objectRest } = object
// a 1
// b 1
// c 1
// objectRest {d: 1, e: 1}

 

새로운 이름으로 다시 할당하는 것 또한 가능.

const object = {
  a: 1,
  b: 1,
}

const { a: first, b: second } = object
// first 1
// second 2

 

기본값 주는 것도 가능.

const object = {
  a: 1,
  b: 1,
}

const { a = 10, b = 10, c = 10 } = object
// a 1
// b 1
// c 10

 

이러한 방식은 리액트 컴포넌트인 props에서 값을 바로 꺼내올 때 매우 자주 쓰는 방식.

function SampleComponent({ a, b }) {
  return a + b
}

SampleComponent({ a: 3, b: 5 }) // 8

 

속성 이름 방식도 가능.

const key = 'a'
const object = {
  a: 1,
  b: 1,
}

const { [key]: first } = object
// first 1

 

이러한 계산된 속성 이름을 사용하려면 반드시 이름을 선언하는 : first와 같은 변수 네이밍이 필요. 그렇지 않으면 에러 발생. 계산된 이름인 [key]로 값을 꺼내기만 했을 뿐, 어느 변수명으로 할당해야 할지 알 수 없기 때문.

const {[key]} = object // Uncaught SyntaxError: Unexpected token '['

 

전개 연산자 ...를 사용하면 나머지 값 모두 가져올 수 있음.

const object = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1,
}

const { a, b, ...rest } = object
// rest {c: 1, d: 1, e: 1}

const { ...rest, a, b } = object
// Uncaught SyntaxError: Rest element must be last element

 

객체 분해 할당 코드가 바벨에서 어떻게 트랜스파일되는지 보자.

// before
const object = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1,
}

const { a, b, ...rest } = object

// after
function _objectWithoutProperties(source, excluded) {
  if (source == null} return {}
  var target = _objectWithoutPropertiesLoose(source, excluded)
  var key, i
  if (Object.getOwnPropertySymbols) {
    var sourceSymbolKeys = Object.getOwnPropertySymbols(source)
    for (i = 0; i < sourceSymbolKeys.length; i++) {
      key = sourceSymbolKeys[i]
      if (excluded.indexOf(key) >= 0) continue
      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue
      target[key] = source[key]
    }
  }
  return target
}

function _objectWithoutPropertiesLoose(source, excluded) {
  if (source == null) return {}
  var target = {}
  var sourceKeys = Object.keys(source)
  var key, i
  for (i = 0; i < sourceKeys.length; i++) {
    key = sourceKeys[i]
    if (excluded.indexOf(key) >= 0) continue
    target[key] = source[key]
  }
  return target
}

var object = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1,
}

var a = object.a,
    b = object.b,
    rest = _objectWithoutProperties(object, ['a', 'b'])

 

배열의 경우와 다르게 조금 더 복잡함. 먼저 _objectWithoutPropertiesLoose는 객체와 해당 객체에서 제외할 키가 포함된 배열 두 가지를 인수로 받는데, 이 두 가지 값을 활용해서 해당 객체에서 특정 키를 제외하는 것. 그리고 _objectWithoutProperties 함수는 Object.getOwnPropertySymbols가 존재하는 환경인 경우(즉 객체 내부에서 심볼의 존재를 확인할 수 잇는 경우)를 대비해 이에 대한 예외 처리 또한 추가로 해줌.

 

이처럼 객체 구조 분해 할당의 경우 트랜스파일을 거치면 번들링 크기가 상대적으로 크기 때문에 만약 자신의 웹 애플리케이션 개발 환경이 ES5를 고려해야 하고, 또 객체 구조 분해 할당을 자주 쓰지 않는다면 꼭 써야 하는지 검토할 필요 있음. 만약 트랜스파일은 부담스럽지만 객체 구조 분해 할당을 통한 ...rest와 같은 함수가 필요하다면 외부 라이브러리 사용 고려해봄직. 유명한 라이브러리로는 lodash.omit이나 rambda.omit이 있음.