1장 리액트 개발을 위해 꼭 알아야 할 자바스크립트
React Deep Dive ·2024년 새해, 새로운 마음 가짐으로 모던 리액트 Deep Dive 책을 정독하기로 해본다. 프런트엔드 분야는 다른 기술 분야에 비해 진입장벽이 상대적으로 낮고 웹서비스를 손쉽고 빠르게 만들 수 있다는 장점 덕분에 많은 사람들, 특히 비전공자들이 진입하는 경우가 많다. 하지만, 리액트와 자바스크립트 사이에 겉으로 드러나지 않는 사실 을 완벽하게 이해하는 과정에 도전하게 된다면 다른 기술 분야와 마찬가지로 프런트엔드 역시 절대 쉽지 않다는 것을 깨닫게 된다. 이 책을 통해 리액트에 대해서 다시 살펴보며 생각을 정리해본다.
1장에서는 리액트 코드의 기반이 되는 자바스크립트에 대해 먼저 알아본다. 그러나 단순히 리액트 코드를 작성하는 데 그치지 않고, 웹 애플리케이션이 작동하는 이면에서 리액트가 수행하는 작업을 이해하려면 자바스크립트의 개념을 다시금 짚어볼 필요가 있다.
1.1 자바스크립트의 동등 비교
- 리액트 함수형 컴포넌트와 훅을 반복적으로 작성하다 보면 의존성 배열(dependencies)에 대해 고민해 본 적이 있을 것이다. 실제로 이것이 어떤 식으로 작동하는지, 또한 왜 이러한 변수들을 넣어야 하는지 이해하지 못하는 경우가 많다.
- 또 렌더링 관점에서도 살펴볼 만한 이유가 있다. 리액트 컴포넌트의 렌더링이 일어나는 이유 중 하나가 바로 props의 동등 비교에 따른 결과다. 그리고 이 props의 동등 비교는 객체의 얕은 비교를 기반으로 이뤄지는데, 이 얕은 비교가 리액트에서 어떻게 작동하는지 이해하지 못하면 렌더링 최적화에 어려움을 겪을 가능성이 크다.
- 리액트의 가상 DOM과 실제 DOM의 비교, 리액트 컴포넌트가 렌더링할지를 판단하는 방법, 변수나 함수의 메모이제이션 등 모든 작업은 자바스크립트의 동등 비교를 기반으로 한다. 자바스크립트의 이러한 동등 비교는 어떻게 수행되는지, 또 이를 리액트에서 어떻게 활용하고 있는지 살펴보자.
1.1.1 자바스크립트의 데이터 타입
- 자바스크립트의 모든 값은 데이터 타입을 갖고 있으며, 이 데이터 타입은 크게 원시 타입과 객체 타입으로 나눌수 있다.
- 원시 타입
- boolean, null, undefined, number, string, symbol, bigint
- 객체 타입
- object
- 원시 타입
객체 타입
- 객체 타입을 간단하게 정의하면 앞서 7가지 원시 타입 이외의 모든 것, 즉 자바스크립트를 이루고 있는 대부분의 타입이 바로 객체 타입이다.
- 여기서 한 가지 주목할 것이 객체 타입(object type)은 참조를 전달한다고 해서 참조 타입(reference type)으로도 불린다는 사실이다.
typeof [] ==== 'object' // true
typeof {} === 'object' // true
function hello() {}
typeof hello === 'function' // true
const hello1 = function () {}
const hello2 = function () {}
// 객체인 함수의 내용이 육안으로는 같아 보여도 참조가 다르기 때문에 false가 반환된다.
hellol === hello2 // false
1.1.2 값을 저장하는 방식의 차이
- 원시 타입과 객체 타입의 가장 큰 차이점이라고 한다면, 바로 값을 저장하는 방식의 차이다. 이 값을 저장하는 방식의 차이가 동등 비교를 할 때 차이를 만드는 원인이 된다.
- 먼저 원시 타입은 불변 형태의 값으로 저장된다. 그리고 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장된다.
let hello = 'hello world'
let hi = hello
console.log(hello === hi) // true
- hello의 hello world라는 값이 hi에 복사해 전달됐기 때문이다. 값을 비교하기 때문에, 값을 전달하는 방식이 아닌 각각 선언하는 방식으로도 동일한 결과를 볼수 있다.
let hello = 'hello world'
let hi = 'hello world'
console. log(hello === hi) // true
- 반면 객체는 프로퍼티를 삭제, 추가, 수정할 수 있으므로 원시 값과 다르게 변경 가능한 형태로 저장되며, 값을 복사할 때도 값이 아닌 참조를 전달하게 된다.
// 다음 객체는 완벽하게 동일한 내용을 가지고 있다.
var hello = {
greet: 'hello, world',
}
var hi = {
greet:'hello, world',
}
// 그러나 동등 비교를 하면 false가 나온다.
console.log(hello === hi) // false
// 원시값인 내부 속성값을 비교하면 동일하다.
console.log(hello.greet === hi.greet) // true
- 따라서 자바스크립트 개발자는 항상 객체 간에 비교가 발생하면, 이 객체 간의 비교는 우리가 이해하는 내부의 값이 같다 하더라도 결과는 대부분 true가 아닐 수 있다는 것을 인지해야 한다.
1.1.3 자바스크립트의 또 다른 비교공식, Object.is
-
자바스크립트에서는 비교를 위한 또 한 가지 방법을 제공하는데, 바로 Object.is다. Object.is는 두 개의 인수를 받으며, 이 인수 두 개가 동일한지 확인하고 반환하는 메서드다. Object.is가 ==나 ===와 다른 점은 다음과 같다.
- == vs. Object.is: == 비교는 같음을 비교하기 전에 양쪽이 같은 타입이 아니라면 비교할 수 있도록 강제로 형변환 (type casting)을 한 후에 변경한다. 따라서 5 == ‘ 5’와 같이 형변환 후에 값이 동일하다면 ==는 true를 반환한다. 하지만 Object.is는 이러한작업을하지 않는다. 즉. === 와동일하게 타입이 다르면 그냥 false다.
- === VS. Object.is: 이 방법에도 차이가 있다. 다음코드를 보면 알수 있듯, Object.is가좀 더 개발자가 기대하는 방식으로 정확히 비교한다.
-0 === +0 // true
Object.is(-0, +0) // false
Number.NaN === NaN // false
Object.is(Number.NaN, NaN) //true
NaN === 0 / 0 // false
Object.is(NaN, 0/0) //true
- 한 가지 주의해야 할 점은, Object.is를 사용한다 하더라도 객체 비교에는 별 차이가 없다는 것이다. 객체 비교는 앞서 이야기한 객체 비교 원리와 동등하다.
1.1.4 리액트에서의 동등 비교
- 그렇다면 리액트에서는 동등 비교가 어떻게 이루어질까? 리액트에서 사용하는 동등 비교는 ==나 ===가 아닌 이 Object.is다. Object.is는 ES6에서 제공하는 기능이기 때문에 리액트에서는 이를 구현한 폴리필(Polyfill)을 함께 사용한다.
- 리액트에서의 비교를 요약하자면 Object.is로 먼저 비교를 수행한 다음에 Object.is에서 수행하지 못하는 비교, 즉 객체 간 얕은 비교를 한 번 더 수행하는 것을 알 수 있다. 객체 간 얕은 비교란 객체의 첫 번째 깊이에 존재하는 값만 비교한다는 것을 의미한다.
// Object.is는 참조가 다른 객체에 대해 비교가 불가능하다.
Object.is({ hello: 'world' }, { hello: 'world' }) // false
// 반면 리액트 팀에서 구현한 shallowEqual은 객체의 1 depth까지는 비교가 가능하다.
shallowEqual({ hello: 'world' }, { hello: 'world' }) // true
// 그러나 2 depth까지 가면 이를 비교할 방법이 없으므로 false률 반환한다.
shallowEqual({ hello: { hi: 'world' } }, { hello: { hi: 'world' } }) // false
- 이렇게 객체의 얕은 비교까지만 구현한 이유는 무엇일까? 먼저 리액트에서 사용하는JSX props는 객체이고, 그리고 여기에 있는 props만 일차적으로 비교하면 되기 때문이다. 다음 코드를 살펴보자.
- 기본적으로 리액트는 props에서 꺼내온 값을 기준으로 렌더링을 수행하기 때문에 일반적인 케이스에서는 얕은 비교로 충분할 것이다. 이러한 특성을 안다면 props에 또 다른 객체를 넘겨준다면 리액트 렌더링이 예상치 못하게 작동한다는 것을 알수 있다.
import { memo, useEffect, useState } from 'react'
type Props = {
counter: number
}
const Component = memo((props: Props) => {
useEffect(() => {
console.log('Component has been rendered!')
})
return <hl>{props.counter}</hl>
})
type DeeperProps = {
counter: {
counter: number
}
}
const DeeperComponent = memo((props: DeeperProps) => {
useEffect(() => {
console.log('DeeperComponent has been rendered!')
})
return <hl>{props.counter.counter}</hl>
})
export default function App() {
const [, setcounter] = useState(0)
function handleClick() {
setCounter((prev) => prev + 1)
}
return (
<div className="App">
<Component counter={100} />
<DeeperComponent counter= />
<button onClick={handleClick}>+</button>
</div>
)
}
- 이와 같이 props가 깊어지는 경우, 즉 한 객체 안에 또다른 객체가 있을 경우 React.memo는 컴포넌트에 실제로 변경된 값이 없음에도 불구하고 메모이제이션된 컴포넌트를 반환하지 못한다. 즉, Component는 props, counter가 존재하지만, DeeperComponent는 props.counter.counter에 props가 존재한다. 상위 컴포넌트인 App에서 버튼을 클릭해서 강제로 렌더 링을 일으킬 경우, ShallowEqual을 사용하는 Component 함수는 위 로직에 따라 정확히 객체 간 비교를 수행해서 렌더 링을 방지해 주었지만 DeeperComponent 함수는 제대로 비교하지 못해 memo가 작동하지 않는 모습을 볼 수 있다.
- 만약 내부에 있는 객체까지 완벽하게 비교하기 위한 재귀문까지 넣었으면 어떻게 됐을까? 객체 안에 객체가 몇 개까지 있을지 알 수 없으므로 이를 재귀적으로 비교하려 할 경우 성능에 악영향을 미칠 것이다.
1.1.5 정리
- 지금까지 자바스크립트에 존재하는 데이터 타입은 무엇인지, 그리고 이 데이터 타입은 어떻게 저장되며 이값의 비교는 어떻게 수행되는지 살펴봤다.
- 자바스크립트를 기반으로 한 리액트의 함수형 프로그래밍 모델에서도 언어적인 한계를 뛰어넘을 수 없으므로 얕은 비교만을 사용해 비교를 수행해 필요한 기능을 구현하고 있다.
- 이러한 자바스크립트의 특징을 잘 숙지한다면 향후 함수형 컴포넌트에서 사용되는 훅의 의존성 배열의 비교, 렌더링 방지를 넘어선 useMemo와 useCallback의 필요성, 렌더링 최적화를 위해서 꼭 필요한 React.memo를 올바르게 작동시키기 위해 고려해야 할 것들을 쉽게 이해할 수 있을 것이다.
1.2 함수
1.2.1 함수란 무엇인가?
- 자바스크립트에서 함수란 작업을 수행하거나 값을 계산하는 등의 과정을 표현하고, 이를 하나의 블록으로 감싸서 실행 단위로 만들어 놓은 것을 의미한다.
- 리액트에서 컴포넌트를 만드는 함수도 이러한 기초적인 형태를 따르는 것을 알 수 있다.
function Component(props) {
return <div>{props.hello}</div>
}
- Component라고 하는 함수를 선언하고 매개변수로는 일반적으로 props라고 부르는 단일 객체를 받으며 return 문으로 JSX를 반환한다.
1.2.2 함수를 정의하는 4가지 방법
- 자바스크립트에서 함수를 정의하는 방법은 크게 4가지로 나눌 수 있다.
함수 선언문
- 자바스크립트에서 함수를 선언할 때 가장 일반적으로 사용하는 방식이다.
function add(a, b) {
return a + b
}
- 함수 선언문은 표현식이 아닌 일반 문(statement)으로 분류된다. 표현식이란 무언가 값을 산출하는 구문을 의미한다. 즉, 앞선 함수 선언으로는 어떠한 값도 표현되지 않았으므로 표현식이 아닌 문으로 분류된다.
const sum = function sum(a, b) {
return a + b
}
sum(10, 24) // 34
- 위 예제는 마치 sum이라는 변수에 함수 sum을 할당하는, 표현식과 같은 작동을 보였다. 그 이유는 무엇일까?
- 자바스크립트 엔진이 코드의 문맥에 따라 동일한 함수를 문이 아닌 표현식으로 해석하는 경우가 있기 때문이다.
함수 표현식
- 함수 표현식에 대해 알아보기 전에 ‘일급 객체’라는 개념을 알고 있어야 한다. 프로그래밍 세계에서 일급객체란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 의미한다.
- 자바스크립트에서 함수는 일급 객체다. 함수는 다른 함수의 매개변수가 될 수도 있고, 반환값이 될 수도 있으며, 앞에서 본 것처럼 할당도 가능하므로 일급 객체가 되기 위한 조건을 모두 갖추고 있다.
함수 표현식과 선언 식의 차이
- 이 두 가지 방식의 가장 큰 차이는 호이스팅(hoisting) 여부다. 함수의 호이스팅이라 함은, 함수 선언문이 마치 맨 앞단에 작성된 것처럼 작동하는 자바스크립트의 특징을 의미한다.
hello() // hello
function hello() {
console.log('hello')
}
hello() // hello
-
함수의 호이스팅은 함수에 대한 선언을 실행 전에 미리 메모리에 등록하는 작업을 의미한다. 이러한 함수의 호이스팅이라는 특징 덕분에 함수 선언문이 미리 메모리에 등록됐고, 코드의 순서에 상관없이 정상적으로 함수를 호출할 수 있게 된 것이다.
- 반면 함수 표현식은 함수를 변수에 할당했다. 변수도 마찬가지로 호이스팅이 발생한다. 그러나 함수의 호이스팅과는 다르게, 호이스팅되는 시점에서 var의 경우에는 undefined로 초기화한다는 차이가 있다.
- 함수와 다르게 변수는, 런타임 이전에 undefined로 초기화되고, 할당문이 실행되는 시점, 즉 런타임 시점에 함수가 할당되어 작동한다는 것을 알 수 있다.
Function 생성자
const add = new Fiinction('a', 'b', 'return a + b')
add(10, 24) // 34
- Function 생성자 함수를 사용해서 만든 모습은 썩 좋아보이지 않는다. 코드 작성 관점에서만 보더라도 매개변수, 그리고 함수의 몸통을 모두 문자열로 작성해야 한다.
화살표 함수
const add = (a, b) => {
return a + b
}
const add = (a, b) => a + b
- 화살표 함수는 겉보기와 다르게 앞서 언급한 함수 생성 방식과 몇 가지 큰 차이점이 있다.
- 먼저 화살표 함수에서는 constructor를 사용할 수 없다.
- 그리고 화살표 함수에서는 arguments가 존재하지 않는다.
- 그리고 화살표 함수와 일반 함수의 가장 큰 차이점은 바로 this 바인딩이다.
- 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 따르게 된다
1.4 클로저
- 리액트의 클래스형 컴포넌트에 대한 이해가 자바스크립트의 클래스, 프로토타입 , this에 달려 있다면, 함수형 컴포넌트에 대한 이해는 클로저에 달려 있다.
- 함수형 컴포넌트의 구조와 작동 방식, 훅의 원리, 의존성 배열 등 함수형 컴포넌트의 대부분의 기술이 모두 클로저에 의존하고 있기 때문에 함수형 컴포넌트 작성을 위해서는 클로저에 대해 이해하는 것이 필수다.
1.4.1 클로저의 정의
- 먼저 클로저에 대한 정의를 MDN에서 찾아보면 “클로저는 함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합이라고 돼 있다.
- 그러나 리액트에서 함수형 컴포넌트와 훅이 등장한 16.8 버전을 기점으로 이 클로저라는 개념이 리액트에서 적극적으로 사용되기 시작하면서 클로저를 빼놓고서는 리액트가 어떤 식으로 작동하는지 이해하기 어려워졌다. 클로저는 무엇이고, 어떻게 활용되는지 살펴보자.
- “선언된 어휘적 환경”이라는 것은, 변수가 코드 내부에서 어디서 선언됐는지를 말하는 것이다. 이는 앞서 이야기한, 호출되는 방식에 따라 동적으로 결정되는 this와는 다르게 코드가 작성된 순간에 정적으로 결정된다. 클로저는 이러한 어휘적 환경을 조합해 코딩하는 기법이다.
1.4.3 클로저의 활용
- 전역 스코프는 어디서든 원하는 값을 꺼내올 수 있다는 장점이 있지만, 반대로 이야기하면 누구든 접근할 수 있고 수정할 수 있다는 뜻도 된다. 다음 예제를 살펴보자.
var counter = 0
function handleClick() {
counter++
}
- 위 counter 변수는 큰 문제를 가지고 있다. 첫째, 전역 레벨에 선언돼 있어서 누구나 수정할 수 있다. 앞서 예제로 확인했던 것처럼 window.counter를 활용하면 쉽게 해당 변수에 접근할 수 있을 것이다.
- 만약 리액트의 usestate의 변수가 전역 레벨에 저장돼 있으면 어떻게 될까? 자바스크립트를 조금만 아는 사람이라면 누구나 리액트 애플리케이션을 쉽게 망가뜨릴 것이다.
- 리액트가 관리하는 내부 상태 값은 리액트가 별도로 관리하는 클로저 내부에서만 접근할 수 있다.
- 클로저를 활용하면 전역 스코프의 사용을 막고, 개발자가 원하는 정보만 개발자가 원하는 방향으로 노출시킬 수 있다는 장점이 있다.
리액트에서의 클로저
- 그렇다면 리액트 함수형 컴포넌트의 훅에서 클로저는 어떻게 사용될까? 클로저의 원리를 사용하고 있는 대표적 인 것 중 하나가 바로 useState다.
function Component() {
const [state, setState] = useState()
function handleClickO {
// usestate 호출은 위에서 끝났지만,
// setState는 계속 내부의 최신값(prev)을 알고 있다.
// 이는 클로저를 활용했기 때문에 가능하다.
setState((prev) => prev + 1)
}
// ...
- useState 함수의 호출은 Component 내부 첫 줄에서 종료됐는데, setState는 useState 내부의 최신 값을 어떻게 계속해서 확인할 수 있을까? 그것은 바로 클로저가 usestate 내부에서 활용됐기 때문이다.
- 외부 함수(useState)가 반환한 내부 함수(setState)는 외부 함수(useState)의 호출이 끝났음에도 자신이 선언된 외부 함수가 선언된 환경(state가 저장돼 있는 어딘가)을 기억하기 때문에 계속해서 state 값을 사용할 수 있는 것이다.
1.4.4 주의할 점
- 클로저는 굉장히 어렵고, 다루기 쉽지 않은 개념이다. 따라서 클로저를 사용할 때는 주의를 요한다. 먼저 다음 코드를 살펴보자.
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
- 실제로 위 코드를 실행하면 0, 1, 2, 3, 4초 뒤에 5만 출력된다. setTimeout의 익명 함수가 클로저로 i를 잘 따라갈 것 같은데, 모두 5가 되는 이유는 무엇일까?
- 그 이유는 i가 전역 변수로 작동하기 때문이다. var는 for 문의 존재와 상관없이 해당 구문이 선언된 함수 레벨 스코프를 바라보고 있으므로 함수 내부 실행이 아니라면 전역 스코프에 var i가 등록돼 있을 것이다. for 문을 다 순회한 이후, 태스크 큐에 있는 setTimeout을 실행하려고했을 때, 이미 전역 레벨에 있는 i는 5로 업데이트가 완료돼 있다.
- 이를 올바르게 수정하는 방법은 첫째, 함수 레벨 스코프가 아닌 블록 레벨 스코프를 갖는 let으로 수정하는 것이다. let은 기본적으로 블록 레벨 스코프를 가지게 되므로 let i가 for 문을 순회하면서 각각의 스코프를 갖게 된다. 이는 setTimeout이 실행되는 시점에도 유효해서 각 콜백이 의도한 i 값을 바라보게 할 수 있다.
- 이처럼 클로저의 기본 개념, ‘함수와 함수가 선언된 어휘적 환경의 조합’을 주의 깊게 살펴봐야 클로저를 제대로 활용할수 있다.
- 클로저를 사용할 때 한 가지 주의할 점은, 클로저를 사용하는 데는 비용이 든다는 것이다. 클로저는 생성될때마다 그 선언적 환경을 기억해야 하므로 추가로 비용이 발생한다.
- 클로저와 그것에 따르는 비용을 확인하기 위해 다음 예제를 살펴보자. 두 함수는 엄청나게 긴 작업(길이가 천만인 배열)을 동일하게 처리한다. 클로저 유무에 따라 자바스크립트 코드에 어떤 차이가 있는지 살펴보자.
// 일반적인 함수
const aButton = document.getElementByld('a')
function heavyJob() {
const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1)
console.log(longArr.length)
}
aButton.addEventListener('click', heavyJob)
// 클로저라면?
function heavyJobWithClosure() {
const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1)
return function () {
console.log(longArr.length)
}
}
const innerFunc = heavyJobWithClosure()
bButton.addEventListener('click', function () {
innerFunc()
})
- 일반적인 함수와 클로저를 사용한 함수가 실제로 어떤 차이가 있는지 크롬 개발자 도구에서 직접 확인해 볼수 있다. 먼저 일반 함수를 사용한 코드를 확인해 보면 아래와 같다.

- 무거운 직업을 일반적인 함수로 처리했을 때 메모리에 미차는 영향. 메모리의 전체 크기도 작고 실행 전후로도 큰 차이가 없음을 알 수 있다. 다음으로 클로저를 사용한 코드를 확인해 보면 아래와 같다.

- 클로저를 활용하는 함수를 크롬 개발자 도구에서 확인해 보면 클로저를 활용하는 쪽이 압도적으로 부정적인 영향을 미치는 것을 알 수 있다.
- 클로저 heavyJobWithClosure()로 분리해 실행하고, 이를 onClick에서 실행하는 방식인데 이미 스크립트를 실행하는 시점부터 아주 큰 배열을 메모리에 올려두고 시작하는 것을 알 수 있다(약 40MB).
- 클로저의 기본 원리에 따라, 클로저가 선언된 순간 내부 함수는 외부 함수의 선언적인 환경을 기억하고 있어야 하므로 이를 어디에서 사용하는지 여부에 관계없이 저장해 둔다. 실제로는 onClick 내부에서만 사용하고 있지만 이를 알 수 있는 방법이 없기 때문에 긴 배열을 저장해 두고 있는 모습이다.
- 반면 일반 함수의 경우에는 클릭 시 스크립트 실행이 조금 길지만 클릭과 동시에 선언, 그리고 길이를 구하는 작업이 모두 스코프 내부에서 끝났기 때문에 메모리 용량에 영향을 미치지 않았다.
- 클로저의 개념, 즉 외부 함수를 기억하고 이를 내부 함수에서 가져다 쓰는 메커니즘은 성능에 영향을 미친다. 클로저는 공짜가 아니므 로 클로저를 사용할 때는 주의가 필요하다.
1.6 라액트에서 자주 사용하는 자바스크립트 문법
- 일반적인 자바스크립트나 Node.js를 기반으로 한 코드와 리액트 코드를 비교하면 리액트 코드가 상대적으로 독특한 모습을 띤다는 것을 알 수 있다. 이러한 독특함은 JSX 구문 내부에서 객체를 조작하거나 객체의 얕은 동등 비교 문제를 피하기 위해 객체 분해 할당을 하는 등 리액트의 몇 가지 독특한 특징에서 비롯된다. 이 리액트의 독특한 특징을 이해하려면 리액트에서 자주 사용되는 자바스크립트 문법을 이해해야 한다. 그리고 자바스크립트 문법을 이해한다면 반대로 리액트가 어떻게 작동하는지도 이해할 수 있다.
1.6.1 구조 분해 할당
- 구조 분해 할당(Destmcturing assignment)이란 배열 또는 객체의 값을 말 그대로 분해해 개별 변수에 즉시 할당하는 것을 의미한다. 언급한 대로 배열과 객체에서 사용하며, 주로 어떠한 객체나 배열에서 선언문 없이 즉시 분해해 변수를 선언하고 할당하고 싶을 때 사용한다.
배열 구조 분해 할당
const array = [1, 2, 3, 4, 5]
const [first, second, third, ...arrayRest] = array
// first 1
// second 2
// third 3
// arrayRest [4, 5]
- 배열의 구조 분해 할당은,의 위치에 따라 값이 결정된다. 따라서 앞의 예제에서 중간 인덱스에 대한 할당을 생략하고 싶다면 다음과 같이 선언할 수 있다.
const array = [1, 2, 3, 4, 5]
const [first, , , , fifth] = array // 2, 3, 4는 아무런 표현식이 없으므로 변수 할당이 생략돼 있다.
first // 1
fifth // 5
- 이러한 방법은 실수를 유발할 가능성이 커서 일반적으로 배열의 길이가 작을 때 주로 쓰인다. 배열 분해 할당에는 기본값을 선언할 수도 있다. 만약 사용하고자 하는 배열의 길이가 짧거나 값이 없는 경우에는 (undefined) 기본값을사용할 것이다.
객체 구조 분해 할당
- 객체 구조 분해 할당은 말 그대로 객체에서 값을 꺼내온 뒤 할당하는 것을 의미한다. 배열 구조 분해 할당과는 달리, 객체는 객체 내부 이름으로 꺼내온다는 차이가 있다.
const object = {
a: 1,
b: 1,
c:1,
d: 1,
e:1,
}
const { a, b, c, ...objectRest } = object
// a 1
// b 2
// c 3
// 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에서 값을 바로 꺼내올 때 매우 자주 쓰는 방식이기 때문에 반드시 이해하고 있어야 한다.
1.6.2 전개 구문
- 전개 구문(Spread Syntax)은 앞서 소개한 구조 분해 할당과는 다르게 배열이나 객체, 문자열과 같이 순회할 수 있는 값에 대해 말 그대로 전개해 간결하게 사용할 수 있는 구문이다.
- 배열 내부에서 …배열을 사용하면 해당 배열을 마치 전개하는 것처럼 선언하고, 이를 내부 배열에서 활용할 수 있다. 이러한 특징을 활용하면 기존 배열에 영향을 미치지 않고 배열을 복사하는 것도 가능하다.
배열의 전개 구문
const arrl = ['a', 'b']
const arr2 = arr1
arrl === arr2 // true. 내용이 아닌 참조를 복사하기 때문에 true가 반환된다.
const arr1 = ['a', 'b']
const arr2 = [...arrl]
arrl === arr2 // false. 실제로 값만 복사됐을 뿐, 참조는 다르므로 false가 반환된다.
객체의 전개 구문
- 객체에서도 배열과 비슷하게 사용이 가능하다. 객체를 새로 만들 때 이 전개 구문을 사용할 수 있으며, 마찬가지로 객체를 합성하는 데 있어 편리함을 가져다 준다.
const newObj = { ...objl, ...obj2 }
// { "a": 1, "b": 2, "c": 3, "d": 4 }
- 한 가지 중요한 것은 객체 전개 구문에 있어서 순서가 중요하다는 것이다. 위에 있을 때와 아래에 있을 때의 작동의 순서 차이로 인해 전혀 다른 객체가 생성될 수 있다.
const obj = {
a: 1,
b: 1,
c: 1,
d: 1,
e:1,
}
// {a: 1, b: 1, c: 10, d: 1, e: 1}
const aObj = {
...obj,
c: 10,
}
// {c: 1, a: 1, b: 1, d: 1, e: 1}
const bObj = {
c: 10,
...obj
}
- aObj와 bObj의 결괏값은 다른데, 전개 구문 이후에 값 할당이 있다면 전개 구문이 할당한 값을 덮어쓰겠지만 반대의 경우에는 오히려 전개 구문이 해당 값을 덮어쓰는 일이 벌어질 것이다.