01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

JS for React

들어가며

왜 리액트인가


  • 최근에 전자정부 표준 framework로 채택 → 안정성, 유지보수성 확보
  • 명시적 상태변경 , 단방향 바인딩: component change s→ view changes
    • if 상태 변경 → 상태 변화를 명시적으로 일으킨 함수만 찾으면 된다 (데이터 흐름의 변화가 단순)
    • 반대방향도 가능하면 Angular
    • 간단함, 유연함
  • JSX, 간결함, 넓은 커뮤니티 등

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


→ props의 동등비교 (which triggers rerendering) 은 얕은 비교이다

JS Datatypes

  • 원시 (Primitive) 타입: 객체 제외 7개

    • boolean
    • null : 명시적으로 비어 있는 값 (JS 초기 오류로 인해 typeof는 ‘object’이다..)
    • undefined : 선언 후 값 없는 인수에 자동 할당되는 값
    • number : 정수, 소수 다 가능한 \([-2^{53}-1, 2^{53}-1]\)(typeof number ≠ typeof bigint)
    • string: 백틱 사용 시 줄바꿈도 가능 (원시 타입이기에 부분변경 안됨 ex: foo[0] = ‘a’ X)
    • bigint: 2020에 출시된 숫자 뒤에 ‘n’붙이거나 BigInt(..)로 사용, $2^{53}-1$보다 더 크기 가능
    • symbol: 중복되지 않은 고유의 값, 새로 추가됨!
  • 객체 타입 (Object, Reference Type) :

    • object: 배열, 함수, 정규식, 클래스 etc

원시vs객체 : 값을 저장하는 방식의 차이

  • 원시 타입: 불변형태, 할당 시 메모리 영역 차지 → 저장
  • 객체 타입: 변경가능한 형태로 저장 (CRUD), 복사 시 값이 아닌 참조를 전달 (그래서 얕은 복사하면 같은 참조값 바라봄..)

Object.is

  • == (양쪽 타입 알아서 맞춰놓고 비교)보다 확실히 비교 (type 비교도 같이)
  • === 보다 직관적으로 비교
    • -0 === +0 → true
    • Object.is(-0, 0) → false
  • 하지만 객체 비교에는 도움이 되지 않음
    • Object.is({}, {}) → false

리액트에서의 동등 비교 = Object.is임 (objectIs(x, y))

  • but Object.is는 ES6기능이라 polyfil 함께 사용

    • typeof Object.is === 'function' ? Object.is : polyfillIs
  • React는 shallowEqual (using objectIs) 로 객체의 1 depth까지만 확인

    • prop 에 따라 리렌더링 하기 때문 !
    • so prop 안에 또 객체 넣으면 React.memo가 정신 못 차릴 떄 있음
    • recursive 하게 찾으면 안되나? => 성능저하 큰일남
  • 객체 비교의 불완전성 - JS 근본없다고 하는 이유 중 하나! (unlike 하스켈, 스칼라..)

1.2 함수


4 Types of Functions

  1. 함수 선언문
  2. 표현식
  3. Function 생성자 (const add = new Function('a', 'b', 'return a+b')) : 미사용!
  4. 화살표 함수 (ES6)
  • JS 엔진 : 선언문을 표현식처럼 해석할 수도 있음.. (문맥에 따라)
 함수 선언문 (Declaration)함수 표현식 (Expression)
function add(a, b) {}const add = function(){}
호이스팅코드의 순서없이 메모리에 함수 등록(변수등록) hoisting 하지만 undefined 먼저 등록
when to use어디서든 자유롭게 호출, 명시적으로 작성하고 싶을 때관리해야 하는 스코프가 있을 때
  • 표현식: 일급 객체여서 가능

    1. 다른 함수의 매개변수 가능
    2. 반환값 가능
    3. 변수에 할당 가능
  • hoisting: 함수에 대한 선언을 실행 전에 메모리에 등록하는 작엄

화살표 함수

  1. constructor 사용 불가

    const Car = (name) => {
      this.name = name;
    };
    const myCar = new Car("BMW"); // TypeError
    
  2. no arguments

    function hello() {
      console.log(arguments);
    } // hello(1,2,3) 하면 출력함
    const hi = () => {
      console.log(arguments);
    }; // hi(1,2,3) 하면 ReferenceError
    
  3. this binding (자신이 속한 객체 or 생성할 인스턴스 가리키는 값)

    • 원래: 함수 호출에 따라 동적으로 결정 됨 (예: 일반함수 호출 -> 내부의 this = 전역 객체)
    • in 화살표 함수: 함수 자체의 바인딩을 갖지 않음. 내부에서 this 참조 시, 상위 스코프의 this 따름

      class Component extends React.Component {
        constructor(props) {
          super(props);
          this.state = { count: 0 };
        }
      
        functionCounter() {
          console.log(this); // undefined (객체 내부를 의미)
          this.setState({ count: this.state.count + 1 });
        }
      
        arrowCounter = () => {
          console.log(this); // class Component (상위 scope의 this를 따름)
          this.setState({ count: this.state.count + 1 });
        };
      
        render() {
          return (
            <>
              {/** cannot read property of undefined'setState' of undefined  */}
              <button onClick={this.functionCounter}>Function</button>
              {/** 정상작동 */}
              <button onClick={this.arrowCounter}>Arrow</button>
            </>
          );
        }
      }
      
    • babel 트랜스파일링에서도 확인 가능
    • 화살표 함수 : this선언 시점에 (상위 스코프로) 결정, 받아 사용 가능
    • 일반 함수: 호출 시 런타임에 결정

Various Functions

  • 즉시 실행 함수 (IIFE: Immediately Invoked Function Expression), 재호출 불가능

    ((a, b) => {
      console.log(a + b);
    })(1, 2);
    
  • 고차 함수 (일급객체라는 특징을 활용) -> 고차 컴포넌트도 가능

    • 함수를 인자로 받거나 함수를 반환하는 함수
    • map, filter, reduce
    const add = (a) => (b) => a + b;
    const add(10)(20); // 30
    const add10 = add(10);
    
  1. 부수효과 줄일 것 -> 완전히는 불가능 하지만 최대한 퓨어하게~
  2. 가능한 작게 함수 만들 것 (코드 냄새 피하기, 재사용성 높이기)

1.3 클래스


  • JS 에서 모든 클래스는 함수로 표현 가능
  • 인스턴스 메서드 - 클래스 내부에 선언, aka 프로토타입 메서드
    • prototype에 선언됨, 고로 prototype chain을 따라가면 찾을 수 있음
  • 정적 메서드 - 클래스 자체에 선언 (인스턴스가 아닌, 이름으로 호출)
    • this.state에 접근 불가
  • 클래스는 ES6부터 지원, 이전에는 프로토타입 활용해 클래스의 작동 방식 구현 가능
  • 클래스는 일종의 문법적 설탕 (syntactic sugar)
  • JS 클래스는 프로토타입 기반으로 작동!

1.4 클로저

  • JS FC 이해에 핵심적인 개념 (구조, 작동방식, 훅의 원리, [deps] 등)

클로저의 정의

Closure : 함수 + 함수 선언된 Lexcial Scope

  • 선언된 어휘적 환경 : 동적 (언제 호출되었는지, this)가 아닌, 정적 (어디서 선언되었는지)으로 결정

변수의 유효범위, 스코프

  • 전역 스코프: 어디서든 접근 가능

    • 전역 스코프는 전역 객체에 바인딩 됨 (window in Browser, global in Node)
  • 함수 스코프

    • 다른 언어들과 달리 블록레벨 ({ }) 따르지 않음

      if (true) {
      var global =global scope
      }
      
      console.log(global) // ’global scope’
      console.log(global === window.global) // true
      
      • 블록 레벨이었다면 ReferenceError

_클로저의 활용

  • ex: useState: 리액트가 별도로 관리하는 클로저 내부에서만 접근 가능한 상태값

  • 활용 시 전역 스코프 사용 막을 수 있음
  • 개발자가 원하는 정보만 공개 가능
function Component() {
  const [count, setCount] = useState(0);
  function onClick() {
    setCount((prev) => prev + 1);
  }
}
  • 클로저가 useState 내부에서 활용되었기 때문에 호출이 끝나도 prev 값을 안다??

    • 외부함수 (useState)가 종료되어도 내부함수 (setCount)가 참조하는 변수는 사라지지 않음
  • 주의점

    • 클로저 사용 시 비용 발생 (선언적 환경을 기억해야하기 때문)
  • 긴 작업을 일반적인 함수로 처리:

    const aButton = document.getElementById("a");
    
    function heavyJob() {
      const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1);
      console.log(longArr.length);
    }
    
    aButton.addEventListener("click", heavyJob);
    
  • 클로저로 처리:

    const aButton = document.getElementById("a");
    
    function heavyJobWithClosure() {
      const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1);
    
      return function () {
        console.log(longArr.length);
      };
    }
    
    const innerFunc = heavyJobWithClosure();
    aButton.addEventListener("click", () => {
      innerFunc();
    });
    

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


  • 동기 - (synch) 직렬
    • 이 요청이 시작된 이후에는 무조건 응답을 받은 이후에야 다른 작업을 처리할 수 있음
    • \(\rightarrow\) JS 기본적으로 동기
  • 비동기 - (async), 병렬
    • 요청 시작 후 응답 여부와 상관없이 다음 작업이 이루어지므로 한 번에 여러 작업이 실행될 수 있음
    • JS 비동기도 가능
    • 근데 사실 병렬이 아니라 걍 기다리지 않고 넘어가는 것!

싱글 스레드 자바스크립트

  • why single-threaded?

    • 초기 의도: HTML 보조역할
    • if multithreading happens -> race condition (경쟁 상태) 발생 가능
  • 비동기 코드 처리 시 오래걸리는 작업 (fetching, reading files etc.) 별도로 처리,

    • 작업 완료 시 콜백함수
    • 이벤트 루프 사용! (효율 증대)
console.log(1);

setTimeout(() => {
  console.log(2);
}, 0);

setTimeout(() => {
  console.log(3);
}, 100);

console.log(4);
  • 답은 1 2 3 4가 아닌, 1 4 2 3

𐃏 Process : 실행 단위
🪡 Thread: 프로세스보다 더 작은 실행 단위하며 메모리를 공유해 여러 작업 동시 수행 가능

  • 옛날: 1 process, 1 work
  • 현재: 하나의 프로세스 -> 여러개 스레드

이벤트 루프

설명 기준: V8 (딱히 ECMAScript에는 없음)

  • JS를 런타임 외부에서 비동기 작업 실행 관리 위해 만들어짐
    • JS 런타임 : JS 실행환경 (browser, nodejs)

Full-width imageMedium: JavaScript Event Loop & its functions

  • 📚 Call Stack: 자바스크립트에서 실행할 코드나 함수를 순차적으로 담아두는 Stack
  • Callback/Task Queue: 비동기 함수의 콜백 / 이벤트 핸들러 담기는 곳
  • 🔄 Event Loop: 호출 스택이 비어 있는지 확인하고, 비어 있을 경우 큐에서 대기 중인 작업을 실행 가장 오래된 애부터 순차적으로 꺼내서 실행
function bar() {
  console.log("bar");
}

function baz() {
  console.log("baz");
}

function foo() {
  console.log("foo");
  bar();
  baz();
}

foo();

CALL STACK기준:

  1. [foo()]
  2. [foo(), console.log]
  3. [foo()] // console log 실행
  4. [foo(), bar()]
  5. [foo(), bar(), console.log]
  6. [foo(), bar()] // console log 실행
  7. [foo()] (bar에 더 이상 없음 -> 제거)
  8. [foo(), baz()]
  9. [foo(), baz(), console.log]
  10. [foo(), baz()] // console log 실행
  11. [foo()] (baz에 더 이상 없음 -> 제거)
  12. [] (foo에 더 이상 없음 -> 제거)
  13. 끗! -> 이벤트 루프가 확인함

그래서 동시는 불가능하고, 순차적으로 함

function bar() {
  console.log("bar");
}

function baz() {
  console.log("baz");
}

function foo() {
  console.log("foo");
  setTimeout(bar, 0);
  baz();
}

foo();

CALL STACK - TASK QUEUE

  1. [foo()]
  2. [foo(), console.log]
  3. [foo()]
  4. [foo(), setTimeout(bar, 0)]
  5. [foo()] - [setTimeout(bar, 0)]
  6. [foo(), baz()] - [setTimeout(bar, 0)]
  7. [foo(), baz(), console.log] - [setTimeout(bar, 0)]
  8. [foo(), baz()] - [setTimeout(bar, 0)]
  9. [foo()] - [setTimeout(bar, 0)]
  10. [] - [setTimeout(bar, 0)]
  11. 끗 (이벤트 루프가 확인)
  12. [bar()] - []
  13. [bar(), console.log]
  14. [bar()]
  15. []
  • \(\Rightarrow\) 딱히 0초 뒤에 실행된다는 것을 보장해주지는 않음!!!
    • 원래 큐: FIFO
    • 우리 (테스크) 큐: FIFO 아님
  • 이런 비동기 함수 실행: 메인 스레드가 아니라 별도의 스레드에서 수행됨!!
    • 브라우저나 노드js가 실행
  • => JS 는 싱글스레드, 근데 외부 도움 필요한 언어

Full-width imagemwanmobile 킹쩌는 애니메이션

  • Task Queue, Micro Task Queue

  • 이벤트 루프는 하나의 마이크로 태스크 큐를 가짐

    • 태스크 큐: [setTimeout, setInterval, setImmediate]
    • 마이크로 태스크 큐: [process.nextTick, Promises, queueMicroTask, MutationObserver]
  • 마이크로 테스크 큐가 우선권 보유 (얘가 비어야만 테스크 큐 작업 가능)
  • 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐 실행은 뒤로 미루어짐

  • 마이크로 태스크 큐 → 브라우저 렌더링 → 태스크 큐
function foo() {
  console.log(`foo`);
}

function bar() {
  console.log(`bar`);
}

function baz() {
  console.log(`baz`);
}

setTimeout(foo, 0);

Promise.resolve().then(bar).then(baz); // 실행 순서: bar, baz, foo

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


  • Babel: transpiles JS
    • 최신문법을 다양한 브라우저에서도 일관적으로 지원가능토록

구조 분해 할당

  • Array destructuring

    const arr = [1, 2, 3, 4, 5];
    
    const [first, second, ...arrayRest] = array;
    // arrayRest: [3, 4, 5]
    
    const [first, , , , fifth] = array;
    const array2 = [1, 2];
    const [a = 10, b = 10, c = 10] = array2;
    //a: 1, b: 2, c: 10
    
    • in babel transplilation:
    const array = [1, 2, 3, 4, 5];
    
    //before
    const [first, second, ...arrayRest] = array;
    
    //after
    var first = array[0],
      second = array[1],
      arrayRest = array.slice(2);
    
  • Object Destructuring

    const object = {
      a: 1,
      b: 1,
      c: 1,
      d: 1,
      e: 1,
    };
    
    Babel Transpilation Before & After

    ```js //before 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'])
    
    ```
    

전개 구문 (spread syntax)

  • 순회할 수 있는 값 (ex: 배열, 객체)에 대해 그대로 전개해 간결하게 사용가능케 함
  • 구조분해 할당과 마찬가지로 전개 트랜스파일 시 번들링 커지니 사용에 주의

Array Prototype Methods

  • map, filter, reduce, forEach (Array.prototype._)

    • 배열 값 더럽히지 않고 새로 만들어 사용하기에 안전!
    • ES5부터 사용 -> 별도의 트랜스파일/폴리필 불피요
  • filter : callback 인수로 받음 (truthy 할 때만 원소 반환 )

    const arr = [1, 2, 3, 4, 5];
    const evenArr = arr.filter((item) => item % 2 === 0);
    
  • reduce : callback, 초기값

    const sum = arr.reduce((current, iterator) => {
      return current + item;
    }, 0); // <-- 초기값!!!
    
  • map : callback + 반환함
  • forEach : 반환값 없음 (callback만 실행!)
    • 멈추기 불가능 (unless throw error or stop process)
    • break, return 다 안된다..!
    • WHY? forEach 내 return: 함수의 Return X, 콜백의 return임!!

1.7 선택이 아닌 필수, 타스


use unknown instead of any 😤 (정적 타이핑 사용!)

  • unknown: 모든 값을 할당할 수 있는 top type

    • any처럼 바로 사용하지는 못 함
    • type narrowing 필요

      function doSomething(callback: unkown) {
        if (typeof callback === "function") {
          callback();
        }
        throw new Error("..");
      }
      
  • never : 어떤 타입도 허용 안하는 bottom type
  • type guard를 적극 활용하자

  • instanceof : 특정 클래스의 인스턴스인지 확인
  • typeof : 특정 요소에 대해 자료형 확인

  • in : 객체에 키가 존재하는지

타입 사용처에서는 좁을수록 좋다

Generic

  • 다양한 타입의 데이터를 사용 가능
    • \(\rightarrow\) 코드의 재사용성과 타입 안전성을 높이기
  1. 함수에서의 제네릭 (arg, ret 값 모두)

    function identity<T>(arg: T): T {
      return arg;
    }
    
    let output1 = identity < string > "myString"; // 타입이 string으로 지정
    let output2 = identity < number > 100; // 타입이 number로 지정
    
  2. 인터페이스 제네릭

    interface GenericIdentityFn<T> {
      (arg: T): T;
    }
    
  3. 클래스 제네릭

    class GenericNumber<T> {
      zeroValue: T;
      add: (x: T, y: T) => T;
    }
    
    let myGenericNumber = new GenericNumber<number>();
    

인덱스 시그니쳐

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

    interface Dictionary<T> {
        [key: number]: T;
    }
    
    let keys: keyof Dictionary<number>; // 숫자
    let value: Dictionary<number>['foo']; // 오류, 프로퍼티 'foo'는 타입 'Dictionary<number>'에 존재하지 않습니다.
    let value: Dictionary<number>[42]; // 숫자
    
    // Record<K,T>
    interface Dictionary<T> = Record<number, T>
    
  • Record<K,T> : 객체의 타입 줄이기

    type Hello = Record<"hello" | "hi", string>;
    // 는 아래와 같음
    type Hello2 = {[key in 'hello' | 'hi']:  string}
    
  • 인덱스 시그니처의 문제점

    • 단독 사용 시 (index signature ONLY) 빈 객체 할당해도 에러가 나지 않음
    type ArrStr = {
      [key: string]: string | number,
      [index: number]: string,
    };
    
    const a: ArrStr = {}; // 타입 선언
    
    a["str"]; // 에러 x
    
  • 유연한 대신 타입 안전성을 잃음 (키 이름 잘못 쓰는 둥 휴먼 에러)
  • Index signature는 런타임에 객체의 프로퍼티를 알 수 없는 경우에만 사용할 것