모던 리액트 Deep Dive | 01장. 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.1 자바스크립트의 동등 비교

  • props의 동등 비교

    • 이에 따라 리액트 컴포넌트의 렌더링이 일어남
    • 객체의 얕은 비교를 기반으로 이뤄짐
    • 얕은 비교가 어떻게 작동하는지 모르면 렌더링 최적화하기 어려움

1.1.1 자바스크립트의 데이터 타입

원시 타입

boolean, null, undefined, number, string, symbol, bigint (7개)

  • 객체가 아닌 모든 타입
  • 객체가 아니기에 이 타입들은 메서드를 갖지 않음
  • undefined

    • 선언 후 값을 할당하지 않은 변수
    • 값이 주어지지 않은 인수에 자동으로 할당되는 값
  • null

    • 아직 값이 없거나 비어있는 값 표현
    • 특별한 점

      typeof null === 'object' // true
  • undefined vs null

    • undefined : 선언했지만 할당되지 않은 값
    • null : 명시적으로 비어 있음을 나타내는 값
  • falsy 한 값

    • NaN, 공백 없는 빈 문자열
  • truthy

    • 객체와 배열을 내부에 값이 존재하는지 여부와 상관없이 truthy로 취급됨
  • String

    • template literal

      • 백틱 사용해 표현한 문자열
      • 줄바꿈이 가능
      • 문자열 내부에 표현식을 쓸 수 있음
    • 원시타입, 변경 불가능 (한 번 생성 후 문자열 변경 불가)
  • Symbol

    • 중복되지 않는 어떤 고유한 값

객체 타입

object

  • 7가지 원시 타입 이외의 모든 것
  • 배열, 함수, 정규식, 클래스 등 포함
  • 참조를 전달해 참조 타입(reference type)으로도 불림

1.1.2 값을 저장하는 방식의 차이

원시 타입

  • 불변 형태의 값으로 저장됨

객체 타입

  • 프로퍼티를 삭제, 추가, 수정 가능해 변경 가능한 형태로 저장됨
  • 값을 복사할 때도 값이 아닌 참조를 전달함

⇒ 객체 간 비교 발생 시 내부 값이 같더라도 결과는 대부분 false일 수 있음을 인지해야 함

1.2 함수

1.2.2 함수를 정의하는 4가지 방법

함수 선언문

  • 가장 일반적으로 사용하는 방식
function add(a, b) {
  return a + b
}
  • 표현식이 아닌 일반 문(statement)으로 분류됨
  • 어떠한 값도 표현되지 않았음

함수 표현식

  • 자바스크립트에서 함수는 일급 객체

    • 함수는 다른 함수의 매개변수/반환값이 될 수도 있고, 할당도 가능하므로 일급 객체의 조건을 모두 갖춤
  • 함수 표현식 vs 선언 식

    • 함수의 호이스팅 : 함수에 대한 선언을 실행 전에 미리 메모리에 등록하는 작업
    • 함수와 다르게 변수는, 런타임 이전에 undefined로 초기화되고, 할당문이 실행되는 런타임 시점에 함수가 할당돼 작동함

Function 생성자

const add = new Function('a', 'b', 'return a + b')
  • 생성자 방식으로 함수 생성 시 함수의 클로저 또한 생성 X
  • 권장되지 않음

화살표 함수

const add = (a, b) => a + b
  • 화살표 함수에서는 constructor를 사용할 수 없음
  • 화살표 함수와 일반 함수의 가장 큰 차이점 : this 바인딩

    ⇒ 별도의 작업을 추가로 하지 않고 this 접근 가능

1.2.3 다양한 함수 살펴보기

즉시 실행 함수 (IIFE: Immediately Invoked Function Expression)

(function (a, b) {
  return a + b
})(10, 24)

((a, b) => {
    return a + b
  },
)(10, 24)
  • 한 번 선언하고 호출된 이후 더 이상 재호출 불가해 일반적으로 이름을 붙이지 않음
  • 즉시 실행 함수의 특성 활용 시 글로벌 스코프를 오염시키지 않는 독립적인 함수 스코프를 운용할 수 있음
  • 함수의 선언과 실행이 바로 그 자리에서 끝나 IIFE 내부의 값은 그 함수 내부가 아니고서는 접근이 불가
  • 일단 선언돼 있으면 어디서 쓸지 모르는 일반 함수와 달리 선언만으로도 실행이 거기서 끝난다는 것을 각인시킬 수 있음

고차 함수

  • 자바스크립트의 함수가 일급 객체라는 특징 활용 → 함수를 인수로 받거나 결과로 새로운 함수를 반환시킬 수 있음
// 함수를 매개변수로 받는 대표적인 고차 함수
const doubledArray = [1, 2, 3].map((item) => item * 2)
  • 이를 활용해 함수형 컴포넌트를 인수로 받아 새로운 함수형 컴포넌트를 반환하는 고차함수도 생성 가능
  • 고차 함수형 컴포넌트 생성 시 컴포넌트 내부에서 공통으로 관리되는 로직을 분리해 관리 가능해 효율적으로 리팩터링 가능

1.2.4 함수를 만들 때 주의해야 할 사항

함수의 부수 효과를 최대한 억제하라

  • 함수의 부수효과(side-effect)

    • 함수 내의 작동으로 인해 함수가 아닌 함수 외부에 영향을 끼치는 것
  • 순수 함수

    • 부수 효과가 없는 함수
    • 언제 어디서나 어떠한 상황에서든 동일한 인수에 동일한 결과를 반환
    • 언제 실행되든 항상 결과가 동일해 예측 가능하며 안정적
  • 비순수 함수

    • 부수 효과가 존재하는 함수

가능한 한 함수를 작게 만들어라

  • 함수당 코드의 길이가 길어질수록 문제를 일으킬 여지가 있는 코드가 생길 확률이 커지고, 내부에서 발생하는 일을 추적하기 어려워짐
  • 중첩이 얼마나 많이 있고 콜백이 얼마나 많은지 eslint의 max-lines-per-function에서 체크
  • 하나의 함수에서 너무 많은 일을 하지 않아야 함
  • 그래야 함수의 원래 목적인 재사용성을 높일 수 있음
  • 가능한 한 함수의 크기를 작게 하는 것이 좋음

누구나 이해할 수 있는 이름을 붙여라

  • 리액트의 useEffect, useCallback 등의 훅의 콜백 함수에 네이밍을 붙여주는 게 가독성에 도움이 됨

    useEffect(function apiRequest() {
      // ... do something
    }, [])
    • apiRequest에 접근할 순 없지만 가독성 높아짐

1.2.5 정리

1.3 클래스

1.3.1 클래스란 무엇인가?

  • 특정한 객체를 만들기 위한 일종의 템플릿
  • 특정한 형태의 객체를 반복적으로 만들기 위해 사용됨
  • 클래스 활용 시 객체를 만드는 데 필요한 데이터나 이를 조작하는 코드를 추상화해 객체 생성을 더욱 편리하게 할 수 있음
class Car {
  // ...
  
  static hello() {
	  console.log('저는 자동차입니다.')
  }
}

const myCar = new Car('자동차')

// 정적 메서드는 클래스에서 직접 호출함
Car.hello()

// 정적 메서드는 클래스로 만든 객체에서는 호출할 수 없음
myCar.hello()

constructor

  • 생성자
  • 이름에서 알 수 있듯이 객체 생성 시 사용하는 특수한 메서드
  • 단 하나만 존재할 수 있으며, 여러 개 사용 시 에러 발생
  • 수행할 필요 없다면 생략 가능

인스턴스 메서드

  • 클래스 내부에서 선언한 메서드
  • 자바스크립트의 prototype에 선언되므로 프로토타입 메서드로 불리기도 함

정적 메서드

  • 클래스의 인스턴스가 아닌 이름으로 호출할 수 있는 메서드
  • 정적 메서드 내부의 this는 클래스로 생성된 인스턴스가 아닌, 클래스 자신을 가리키기 때문에 다른 메서드에서 일반적으로 사용하는 this를 사용할 수 없음
  • 장점

    • 비록 this에 접근할 수 없지만 인스턴스 생성 없이도 사용 가능
    • 새성하지 않아도 접근 가능해 객체 생성 없이도 여러 곳에서 재사용 가능

    ⇒ 애플리케이션 전역에서 사용하는 유틸 함수를 정적 메서드로 많이 활용

1.3.2 클래스와 함수의 관계

  • 클래스가 작동하는 방식은 자바스크립트의 프로토타입을 활용하는 것

1.4 클로저

각 컴포넌트 이해에 중요한 것
클래스형 컴포넌트 클래스, 프로토타입, this
함수형 컴포넌트 클로저

1.4.1 클로저의 정의

  • 함수형 컴포넌트와 훅이 등장한 16.8 버전을 기점으로 클로저 개념이 리액트에서 적극적으로 사용되기 시작함
  • 선언된 렉시컬 스코프 : 변수가 코드 내부에서 어디서 선언됐는지
  • 호출되는 방식에 따라 동적으로 결정되는 this와는 다르게 코드가 작성된 순간에 정적으로 결정됨

    ⇒ 클로저는 이러한 어휘적 환경을 조합해 코딩하는 기법

1.4.2 변수의 유효 범위, 스코프

  • 스코프 : 변수의 유효범위

전역 스코프

  • 전역 레벨에 선언하는 것
  • 이 스코프에 변수 선언 시 어디서든 호출 가능
전역 객체
브라우저 환경 window
Node.js 환경 global
  • 이 전역 객체에 전역 레벨에서 선언한 스코프가 바인딩됨

함수 스코프

  • JS는 기본적으로 함수 레벨 스코프를 따름
  • {} 블록이 스코프 범위를 결정하지 않음

    if (true) {
      var global = 'global scope'
    }
    
    console.log(global) // 'global scope'
    console.log(global === window.global) // true
  • var global은 분명 {} 내부에서 선언돼 있는데, {} 박에서도 접근이 가능함

    ⇒ JS는 함수 레벨 스코프를 가지기 때문

  • if 블록과 달리 함수 블록 내부에서는 예측대로 스코프가 결정됨
  • 스코프는 일단 가장 가까운 스코프에서 변수 존재를 확인함

1.4.3 클로저의 활용

function outerFunction() {
  var x = 'hello'
  function innerFunction() {
	  console.log(x)
  }
  
  return innerFunction
}

const innerFunction = outerFunction()
innerFunction() // 'hello'
  • 반환된 innerFunction에는 x 변수가 존재하지 않지만, 해당 함수가 선언된 렉시컬 스코프(outerFunction)에는 x가 존재해 접근 가능
  • 같은 환경에서 선언되고 반환된 innerFunction에서는 x라는 변수가 존재하던 환경을 기억해 정상적으로 ‘hello’ 출력 가능

클로저의 활용

  • 리액트가 관리하는 내부 상태 값은 리액트가 별도로 관리하는 클로저 내부에서만 접근 가능
  • 클로저 활용 시 전역 스코프의 사용을 막고, 개발자가 원하는 정보만 개발자가 원하는 방향으로 노출시킬 수 있음
  • useState를 저장해 두고, useState의 변수 접근 및 수정 또한 클로저 내부에서 확인 가능해 값이 변하면 렌더링 함수 호출 등의 작업이 이뤄짐

리액트에서의 클로저

  • useState: 클로저의 원리를 사용하는 대표적인 것

    function Component() {
      const [state, setState] = useState()
      
      function handleClick() {
        // useState 호출은 위에서 끝났지만 setState는 계속 내부의 최신값(prev)을 알고 있음
        // 클로저를 활용했기 때문에 가능
        setState((prev) => prev + 1)
      }
    }
    • useState 함수의 호출은 내부 첫 줄에서 종료됨
    • 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)
}

⇒ 5만 출력됨

  • setTimeout의 익명 함수가 클로저로 i를 따라가지 않는 이유 : i가 전역 변수로 작동하기 때문
  • JS는 함수 레벨 스코프를 따르기 때문에 var는 for문의 존재와 무관하게 해당 구문이 선언된 함수 레벨 스코프를 보기 때문에 전역 스코프에 var i가 등록됨

    ⇒ for 문 다 순회한 이후, 태스크 큐에 있는 setTimeout을 실행하려 했을 때, 이미 전역 레벨에 있는 i는 5로 업데이트가 완료돼있음

수정하기

  1. 함수 레벨 스코프가 아닌 블록 레벨 스코프를 갖는 let으로 수정

    for (let i = 0; i < 5; i++) {
      setTimeout(function () {
        console.log(i)
      }, i * 1000)
    }
    • let은 기본적으로 블록 레벨 스코프를 가져 let i가 for문을 순회하며 각각의 스코프를 갖게 됨
    • setTimout이 실행되는 시점에도 유효해 각 콜백이 의도한 i 값을 바라보게 할 수 있음
  2. 클로저를 제대로 활용

    for (var i = 0; i < 5; i++) {
      setTimeout(
        (function (sec) {
    	    return function () {
    	      console.log(sec
    	    }
        }(i),
        i * 1000,
      )
    }
    • for 문 내부에 즉시 실행 익명 함수를 선언
    • 이 즉시 실행 함수는 i를 인수로 받는데, 이 함수 내부에서 이를 sec이라고 하는 인수에 저장해뒀다 setTimeout의 콜백 함수에 넘기게 됨
    • setTimeout의 콜백 함수가 바라보는 클로저는 즉시 실행 익명 함수가 됨

      ⇒ 각 for 문마다 생성되고 실행되기를 반복

    • 각각의 함수는 고유한 스코프, 즉 고유한 sec을 가지게 되어 올바르게 실행 가능

    주의할 점

    • 클로저는 사용하는 데 비용이 듬
    • 생성될 때마다 그 선언적 환경을 기억해야 하므로 추가로 비용이 발생
    • 클로저의 기본 원리에 따라, 클로저가 선언된 순간 내부 함수는 외부 함수의 선언적 환경을 기억해야 하기 때문에 이를 어디에서 사용하는지 여부에 무관하게 저장해 둠
    • 실제로는 특정 함수 내부에서만 사용하더라도 이를 알 수 있는 방법이 없어 긴 배열을 저장해 둠
    • 반면 일반 함수는 클릭 시 스크립트 실행이 조금 길지만 클릭과 동시에 작업이 모두 스코프 내부에서 끝나 메모리 용량에 영향을 미치기 않을 수 있음

    1.4.5 정리

    1.5 이벤트 루프와 비동기 통신의 이해

    • JS는 싱글 스레드에서 작동 → JS는 한 번에 하나의 작업만 동기 방식으로 처리할 수 있음
    • 동기(synchronous) : 직렬 방식으로 작업을 처리하는 것

      • 이 요청 시작 이후에는 무조건 응답을 받은 이후에야 비로소 다른 작업 처리 가능
      • 직관적이지만 한 번에 다양한 많은 작업 처리 불가
    • 비동기(asynchronous) : 병렬 방식으로 작업을 처리하는 것

      • 요청 시작 후 응답 오건 말건 상과넚이 다음 작업이 이루어지며, 따라서 한 번에 여러 작업 실행 가능
    • 모던 웹 애플리케이션에서는 사용자에게 많은 양의 정보를 다양한 방식으로 제공하기 위해 많은 것이 비동기로 작동함

      • ex. 검색을 위한 네트워크 요청이 발생하는 순간에도 사용자는 다른 작업 처리 가능
    • 리액트

      • 과거 렌더링 스택을 비우는 방식으로 구현됐던 동기식의 렌더링이 16버전에서 비동기식으로 작동하는 법도 소개됨

        ⇒ 리액트에도 비동기식으로 작동하는 작업이 존재

    1.5.1 싱글 스레드 자바스크립트

    • 과거

      • 프로그램을 실행하는 단위가 오직 프로세스뿐
      • 프로세스 : 프로그램을 구동해 프로그램의 상태가 메모리상에서 실행되는 작업 단위
      • 하나의 프로그램 실행 : 하나의 프로세스를 가지고 그 프로세스 내부에서 모든 작업이 처리되는 것을 의미
    • 소프트웨어의 복잡성 증가 → 동시에 여러 개의 복잡한 작업을 수행할 필요성

      ⇒ 그래서 탄성한 더 작은 실행 단위 : 스레드

    • 하나의 프로세스에서는 여러 개의 스레드를 만들 수 있고, 스레드끼리는 메모리를 공유할 수 있어 여러 가지 작업을 동시에 수행 가능
    • 멀티 스레드 : 내부적으로 처리가 복잡하다는 단점

      • 스레드는 하나의 프로세스에서 동시에 서로 같은 자원에 접근 가능
      • 동시에 여러 작업 수행 시 같은 자원에 대해 여러 번 수정하는 등 동시성 문제가 발생 가능해 처리 필요
      • 하나의 스레드가 문제가 생기면 같은 자원 공유하는 다른 스레드에도 동시에 문제 발생 가능
    • 최초의 자바스크립트

      • 브라우저에서 HTML을 그리는 데 한정적인 도움을 주는 보조적인 역할
      • 아주 기초적인 수준에서만 제한적으로 사용
      • 설계 당시에는 약 30년 뒤 현재처럼 복잡할 거라고 예상 못했을 것
    • JS가 멀티 스레딩을 지원해 동시에 여러 스레드가 DOM 조작이 가능하다면?

      • 멀티 스레딩은 메모리 공유로 인해 동시에 같은 자원에 접근하면 타이밍 이슈 발생 가능

        ⇒ 브라우저의 DOM 표시에 큰 문제 야기 가능

    • 싱글 스레드 : JS 코드의 실행이 하나의 스레드에서 순차적으로 이루어짐
    • Run-to-completion : 하나의 코드 실행 시 오래 걸리면 뒤이은 코드가 실행되지 않음
    • async(asynchronous) : 비동기 함수 선언 시 사용

      • 동시에 일어나지 않는 것
      • 동기식과 다르게 요청 즉시 결과가 주어지지 않을 수 있고, 응답이 언제 올지 알 수 없음
      • 동기식과 다르게 여러 작업을 동시에 수행 가능

    1.5.2 이벤트 루프란?

    • JS 표준(ECMAScript)에 나와 있는 내용은 아님
    • JS 런타임 외부에서 JS의 비동기 실행을 돕기 위해 만들어진 장치

    콜스택과 이벤트 루프

    • 콜스택

      • JS에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택
    • 이벤트 루프

      • 호출 스택이 비어 있는지 여부를 확인하는 것
      • 단순히 이벤트 루프의 단일 스레드 내부에서 이 콜스택 내부에 수행해야 할 작업이 있는지 확인, 수행해야 할 코드가 있다면 JS 엔진을 이용해 실행
      • ‘코드를 실행하는 것’과 ‘호출 스택이 비어있는지 확인하는 것’ 모두 단일 스레드에서 일어남
      • 두 작업은 동시 발생이 불가해 한 스레드에서 순차적으로 일어남
      • 이벤트 루프가 콜스택이 비워진 것을 확인 후 태스크 큐를 확인해 있는 내용을 콜스택에 들여보냄
    • setTimeout(() ⇒ {}, 0) 이 정확히 0초 뒤에 실행된다는 것을 보장하지 못함
  3. 태스크 큐

    • 실행해야 할 태스크의 집합
    • 이벤트 루프는 태스크 큐를 한 개 이상 가짐
    • 이름과 다르게 queue가 아닌 set의 자료구조를 가짐

      ⇒ 선택된 큐 중 실행 가능한 가장 오래된 태스크를 가져와야 하기 때문

    • ‘실행해야 할 태스크’ : 비동기 함수의 콜백 함수나 이벤트 핸들러 등을 의미
  • 비동기 함수는 누가 수행하나

    • n초 뒤 setTimeout을 요청, fetch를 기반으로 실행되는 네트워크 요청은 누가 보내고 응답을 받나

      ⇒ 이 작업들은 모두 JS가 동기식으로 실행되는 메인 스레드가 아닌 태스크 큐가 할당되는 별도의 스레드에서 수행됨

    • 별도의 스레드에서 태스크 큐에 작업을 할당해 처리하는 것 ⇒ 브라우저나 Node.js 의 역할
  • 모두 JS 코드가 실행되는 메인 스레드에서만 이뤄진다면 절대 비동기 작업을 수행할 수 없을 것

1.5.3 태스크 큐와 마이크로 태스크 큐

마이크로 태스크 큐

  • 이벤트 루프는 하나의 마이크로 태스크 큐를 가지며, 기존의 태스크 큐와는 다른 태스크를 처리함
  • 대표적으로 Promise
  • 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 가짐
  • setTimeout, setInterval은 Promise보다 늦게 실행됨
  • 태스크 큐 실행에 앞서 먼저 마이크로 태스크 큐를 실행하고, 이 마이크로 태스크 큐 실행 뒤에 렌더링이 일어남
  • 각 마이크로 태스크 큐 작업 종료 시마다 한 번씩 렌더링할 기회를 얻게 됨

1.5.4 정리

  • JS 코드를 실행하는 것 자체는 싱글 스레드로 이뤄져 비동기를 처리하기 어렵지만 JS 코드 실행 이외에 태스크 큐, 이벤트 루프, 마이크로 태스크 큐, 브라우저/Node.js API 등이 적절한 생테계를 이루고 있어 싱글 스레드로는 불가한 비동기 이벤트 처리가 가능해짐

1.6 리액트에서 자주 사용하는 자바스크립트 문법

  • 리액트의 독특한 특징

    • JSX 구문 내부에서 객체 조작
    • 객체의 얕은 동등 비교 문제를 피하기 위해 객체 분해 할당 사용
  • 바벨

    • 사용자의 다양한 브라우저 환경, 최신 문법을 작성하고 싶은 개발자의 요구를 해결하기 위해 탄생한 것
    • JS의 최신 문법을 다양한 브라우저에서도 일관적으로 지원할 수 있도록 코드를 트랜스파일함

1.6.1 구조 분해 할당

배열 구조 분해 할당

  • 객체 구조 분해 할당은 사용하는 쪽에서 원하는 이름으로 변경하는 것이 번거로움
  • 배열 구조 분해 할당은 자유롭게 이름 선언 가능해 useState가 배열을 반환하는 것으로 추측 가능
  • 배열의 구조 분해 할당은 ,의 위치에 따라 값이 결정됨

    const [first, , , , fifth] = array

    ⇒ 배열의 길이가 작을 때 주로 쓰임

  • 기본값 선언 가능

    const array = [1, 2]
    const [a = 10, b = 10, c = 20] = array
    • 원래 값이 비었거나 undefined일 때만 기본값이 사용됨
  • 특정값 이후의 값을 다시 배열로 선언하고 싶다면 전개 연산자 사용

    const [first, ...rest] = array
    • 예측할 수 있는 뒤쪽에서만 가능
    • 앞쪽이라면 파악 불가해 앞에서 전개 연산자 사용하는 것은 불가능

객체 구조 분해 할당

  • 새로운 이름으로 재할당 가능

    const object = {
      a: 1, 
      b: 1.
    }
    
    const { a: first, b: second } = object
  • 기본값 주는 것도 가능

    const { a = 10, b = 10, c = 10} = object
  • 객체의 경우 구조 분해 할당을 트랜스파일할 경우 조금 더 복잡함
  • 트랜스파일을 거치면 번들링 크기가 상대적으로커서 웹 앱 개발 환경이 ES5를 고려해야 하고, 객체 구조 분해 할당을 자주 쓰지 않는다면 꼭 써야할지 검토가 필요

    • 트랜스파일은 부담스럽지만 객체 구조 분해 할당을 통한 …rest 같은 함수 필요시 외부 라이브러리 고려 가능

1.6.2 전개 구문

  • 배열이나 객체, 문자열과 같이 순회할 수 있는 값에 대해 전개해 간결하게 사용할 수 있는 구문

배열의 전개 구문

  • 과거에는 배열 간 합성 시 push(), concat(), splice() 등의 메서드 사용 필요

    ⇒ 전개 구문 활용 시 쉽게 합성 가능

  • 기존 배열에 영향 없이 값만 복사 가능

    const arr1 = ['a', 'b'] 
    const arr2 = [...arr1]
    
    arr 1 === arr2 // false

    ⇒ 값만 복사되고 참조는 다르므로 false 반환

객체의 전개 구문

  • 객체 전개 구문에 있어 순서가 중요
  • 전개 구문 → 값 할당 : 전개 구문이 할당 값을 덮어쓰지만 반대면 반대로 덮어씀

    const aObj = {
      ...obj,
      c: 10,
    }
    
    const bObj = {
      c: 10,
      ...obj,
    }
  • 트랜스파일 결과

    • 단순히 값을 복사하는 배열과 다르게 객체는 객체의 속성값 및 설명자 확인, 심벌 체크 등 때문에 트랜스파일된 코드가 커지는 것을 볼 수 있음
    • 객체 구조 분해 할당과 마찬가지로, 객체 전개 연산자 또한 트랜스파일되면 상대적으로 번들링이 커지기 때문에 사용할 때 주의할 필요가 있음

1.6.3 객체 초기자

const a = 1
const b = 2

const obj = {
  a, 
  b,
}
  • 객체 초기자를 사용할 경우 객체를 좀 더 간편하게 선언 가능해 매우 유용하며, 트랜스파일 이후에도 큰 부담이 없음

1.6.4 Array 프로토타입의 메서드: map, filter, reduce, forEach

  • map, filter, reduce

    • 기존 배열의 값을 건드리지 않고 새로운 값을 만들어 내 기존 값 변경 염려 없이 안전하게 사용 가능
  • forEach

    • ES에서부터 사용한 문법 → 별도의 트랜스파일이나 폴리필이 없어도 부담 없이 사용 가능

Array.prototype.forEach

  • 반환값이 undefined로 의미 없음
  • forEach 내의 return은 함수의 return이 아닌 콜백 함수의 return으로 간주됨
  • forEach 사용 시에는 절대 중간에 순회를 멈출 수 없음을 인지해야 함

1.7 선택이 아닌 필수, 타입스크립트

1.7.1 타입스크립트란?

  • JS는 기본적으로 동적 타입 언어라 대부분의 에러를 코드 실행 시 확인할 수 있는 문제점이 있음
  • 동적 언어 타입 : 자유를 주지만 발목을 잡기도 함
  • 타입스크립트

    • 타입 체크를 정적으로 런타임이 아닌 빌드(트랜스파일) 타임에 수행할 수 있게 해줌
    • JS의 슈퍼셋으로서 함수의 반환 타입, 배열, enum 등 기존에는 사용하기 어려웠던 타입 관련 작업들을 손쉽게 처리 가능
    • JS의 슈퍼셋일 뿐이라 JS에서 불가능한 일은 TS에서도 불가능
    • TS로 작성된 파일은 결국 JS로 변환돼 Node.js나 브라우저 같은 JS 런타임 환경에서 실행되는 것이 최종 목표

1.7.2 리액트 코드를 효과적으로 작성하기 위한 타입스크립트 활용법

any 대신 unknown을 사용하자

  • TS 작성 시 실수 중 하나 : any를 자주 사용함
  • any는 불가피할 때만 사용해야 하는 타입
  • any 사용

    • 사실상 TS가 제공하는 정적 타이핑의 이점을 모두 버리는 것
    • 코드가 문제가 되는 것은 런타임이 되며, TS 사용의 이점을 모두 없애버림
  • 불가피하게 타입 단정 불가한 경우 unknown을 사용하는 것이 좋음
  • unknown

    • 모든 값을 할당할 수 있는 top type, 어떤 것도 할당 가능
    • unknown으로 선언된 변수 사용 위해서는 type narrowing이 필요
  • never

    • bottom type으로 어떠한 타입도 들어올 수 없음을 의미
    • 코드상으로 존재 불가한 타입을 나타냄

타입 가드를 적극 활용하자

  • 타입을 사용하는 쪽에서는 최대한 타입을 좁히는 것이 좋음
  • 타입 가드 : 타입을 좁히는 데 도움을 줌

    • instanceof : 지정한 인스턴스가 특정 클래스의 인스턴스인지 확인할 수 있는 연산자
    • typeof : 특정 요소에 대해 자료형을 확인하는 데 사용됨
    • in

      • property in object로 사용됨
      • 주로 어떤 객체에 키가 존재하는지 확인하는 용도
      function doSchool(person: Student | Teacher) {
        if ('age' in person) {
      	  // ...
        }
      }

제네릭

  • 함수나 클래스 내부에서 단일 타입이 아닌 다양한 타입에 대응할 수 있게 도와주는 도구
  • 사용 시 타입만 다른 비슷한 작업을 하는 컴포넌트를 단일 제네릭 컴포넌트로 선언해 간결하게 작성 가능
  • useState()와 같은 형식으로 기본값을 넘기지 않고 사용하면 값을 undefined로 추론해버릴 수 있음
  • 제네릭은 하나 이상도 사용 가능 → 적절한 네이밍 필요

    function multipleGeneric<First, Last>(a1: First, a2: Last): [First, Last] {}

인덱스 시그니처

  • 객체의 키를 정의하는 방식

    type Hello = {
      [key: string]: string
    }
    • 존재하지 않는 키로 접근 시 undefined를 반환할 수도 있음
  • Record<Key, Value> 사용 시 객체의 타입에 각각 원하는 키와 값을 넣을 수 있음
  • 객체에 인덱스 시그니처 사용 시 다음 이슈 발생 가능

    Object.keys(hello).map((key) => {
    	// No index signature with a parameter of type 'string' was found on type 'Hello'
      const value = hello[key]
      return value
    })
    • Object.keys가 string[]을 반환하는데, 이 string은 hello의 인덱스 키로 접근할 수 없음
    • 해결법

      1. Object.keys(hello)를 as로 타입 단언하기

        (Object.keys(hello) as Array<keyof Hello>).map((key) => {
        	const value = hello[key]
        	return value
        })
      2. 타입 가드 함수 만들기

        function keysOf<T extends Object>(obj: T): array<keyof T> {
        	return Array.from(Object.keys(obj)) as Array<keyof T>
        }
        
        keysOf(hello).map((key) => {
        	const value = hello[key]
        	return value
        })
      3. 가져온 key를 단언하기

        Object.keys(hello).map((key) => {
          const value = hello[key as keyof Hello]
          return value
        })
    • Object.keys는 함수 내부에서 적절히 추론 가능함에도 왜 string[]으로 강제돼 있나

      ⇒ JS의 특징과, 이를 구현하기 위한 TS의 구조적 타이핑의 특징 때문

      • 덕 타이핑 : 객체의 타입이 클래스 상속, 인터페이스 구현 등으로 결정되지 않고 어떤 객체가 필요한 변수, 메서드만 가지면 해당 타입에 속하도록 인정해주는 것
      • TS의 핵심 원칙 : 타입 체크를 할 때 그 값이 가진 형태에 집중함
      • JS는 객체의 타입에 구애받지 않고 객체의 타입에 열려 있으므로 TS도 이러한 JS의 특징을 맞춰줘야 함

        ⇒ TS는 모든 키가 들어올 수 있는 가능성이 열려 있는 객체의 키에 포괄적으로 대응하기 위해 string[]으로 타입을 제공하는 것

      1.7.3 타입스크립트 전환 가이드

      tsconfig.json 먼저 작성하기

      {
        "compilerOptins": {
      	  "outDir": "./dist",
      	  "allowJs": true,
      	  "target": "es5"
        },
        "include": ["./src/**/*"]
      }
      • outDir: .ts나 .js가 만들어진 결과를 넣어두는 폴더. tsc 실행 시 결과물이 들어감
      • allowJs: .js 파일 허용 여부
      • target: 결과물이 될 JS 버전
      • include: 트랜스파일할 JS와 TS 파일 지정

      JSDoc과 @ts-check를 활용해 점진적으로 전환하기

      • TS 파일로 전환하지 않아도 타입 체크하는 방법
      • 상단에 //@ts-check 선언하고 JsDoc을 활용해 변수나 함수에 타입 제공 시 TS 컴파일러가 JS 파일의 타입을 확인함
      • 특수 케이스 제외하고는 바로 .ts로 파일 확장자를 변경해 바로 작업하는 게 더 빠를 수 있음

      타입 기반 라이브러리 사용을 위해 @types 모듈 설치하기

      • 과거에 JS 기반으로 작성된 라이브러리 설치해 사용 중이라면 TS에서 라이브러리 정상적 사용을 위해 @types라 불리는 DefinitelyTyped 를 설치해야 함
      • DefinitelyTyped

        • TS로 작성되지 않은 코드에 대한 타입을 제공하는 라이브러리
        • 리액트를 TS에서 사용하기 위해서도 이 모듈 설치 필요
      • 리액트에 대한 타입은 @types/react@types/react-dom 등에 정의돼 있음
      • 모든 라이브러리가 @types를 필요로 하는 것은 아님

        • Next.js와 같이 비교적 최근에 만들어진 라이브러리들은 이미 자체적으로 TS 지원 기능이 라이브러리에 내장돼 있음
      • “Cannot find module ‘lodash’ or its corresponding type declarations” 에러

        • 라이브러리 내부에 별도로 d.ts 같은 타입 파일 제공하지 않아서임

      파일 단위로 조금씩 전환하기

      • 먼저 전환: 상수, 유틸 등 별도 의존성을 가지지 않은 파일
      • 파일을 하나씩 TS로 전환하고, 상수는 string, number와 같이 원시값 대신 가능한 한 타입을 좁혀보자
      • JS 기반 코드를 TS로 전환하는 것은 매우 인내심이 필요한 일

        • 하지만 점진적으로 코드 전환 시 잠재적 에러 발생 가능한 코드를 발견하게 될 것

        ⇒ 코드를 하나씩 수정해 나가다 보면 어느새 코드가 더욱 단단해짐을 느낄 수 있음


Written by@jaeeun
I explain with words and code. I explain with words and code. I explain with words and code.