본문 바로가기
Front-End Study/모던 리액트 딥다이브 스터디

모던 리액트 딥다이브 - 3회차 [1-3, 1-4, 1-5]

by 코딩기 2024. 6. 3.
728x90

1.3 클래스

16.8 버전 전까지 모든 리액트의 컴포넌트는 클래스였다.

🔖 함수형 컴포넌트로 변경된지 얼마 되지 않은 리액트를 더 깊이 있게 알아보자.

1.3.1 클래스란 무엇인가?


🔖 특정 객체를 만들기 위한 일종의 템플릿과 같은 개념

  • 객체를 만들 때 필요한 데이터나 조작 코드를 추상화하여 더 편리하게 생성할 수 있도록 한다.
  • JS에서 클래스로 하는 모든 것을 함수로 동일하게 할 수 있다.

🏷️ 클래스 작성법

class Car {
  // constructor (생성자) 작성
  // 최초 생성 시 어떤 인수를 받을지 결정 및 객체 초기화
  constructor(name) {
    this.name = name;
  }

  // 메서드
  honk() {
    console.log(`${this.name}이(가) 경적을 갈긴다.`);
  }

  // 정적 메서드
  static hello() {
    console.log("안녕? 난 차야");
  }

  // setter
  set age(value) {
    this.carAge = value;
  }

  // getter
  get age() {
    return this.carAge;
  }
}

🏷️ 클래스 사용법

// 미리 정의한 Car 클래스를 이용해 객체 생성
const myCar = new Car("포르쉐");

// 메서드 호출
myCar.honk(); // 포르쉐이(가) 경적을 갈긴다.

// 정적 메서드는 클래스에서 직접 호출한다.
Car.hello(); // 안녕? 난 차야

// 정적 메서드는 클래스로 만든 객체에선 호출이 불가하다.
myCar.hello(); // UncaughtError

// setter를 사용해 값을 할당할 수 있다.
myCar.age = 32; // myCar.carAge = 32;

// getter를 사용해 값을 가져올 수 있다.
console.log(myCar.age, myCar.name); // 32 포르쉐

🔖 클래스 내부 특징

  • constructor

  • 생성자로, 객체를 생성하는데 사용
  • 단 하나만 존재 가능(여러개 사용 시 에러)
  • 수행할 작업이 없을 시 생략 가능

  • 프로퍼티

  • 인스턴스 생성 시 내부에 정의할 수 있는 속성값
  • 타입스크립트 활용 시 private, public, prottected 활용 가능

  • getter & setter

  • 클래스에서 값을 가져오거나, 값을 할당할 때 사용
  • 각각 getset 키워드를 사용한다.

  • 인스턴스 메서드

  • 클래스 내부에서 선언한 메서드 (prototype에 선언)
  • prototype에 선언했으므로, 클래스를 기반으로 만드는 객체들은 메서드 또한 사용 가능
  • 비교를 위해 Object.getPrototypeOf 메서드를 사용 가능하다.

❗여기서 prototype 이란?

  • 객체 상속 시 사용하는 객체를 prototype 이라 정의한다.
    • 모든 객체는 proto(비표준)이라는 속성을 가지며, 이는 프로토타입을 가리킨다.
    • 객체의 속성이나 메서드를 검색하고, 없을 시 프로토타입을 참조한다. (프로토타입 체인)

🏷️ ProtoType 관련 포스팅

  • 정적 메서드

  • 클래스의 인스턴스가 아닌, 이름으로 호출 가능한 메서드
    클래스 자신을 가리키므로, this 사용이 불가하고 인스턴스에서도 호출 불가하다.
    객체를 생성하지 않아도 여러 곳에서 사용 가능하여, 전역적 유틸 함수로 활용한다.

  • 상속

  • 부모-자식 관계와 같이 다른 클래스에 속성들을 상속시켜 확장하는 개념
  • extends 키워드를 활용하여 다양하게 파생된 클래스 생성이 가능하다.

1.3.2 클래스와 함수의 관계


🔖 클래스가 작동하는 방식은 JS의 prototype을 활용하는 것

// 프로토타입를 활용한 JS의 클래스 구현
const Car = (function () {
  function Car(name){
    this.name = name;
  }

  Car.prototpye.honk = function () {
    console.log(`${this.name}이(가) 경적을 갈긴다.`);
  }

  Car.hello = function () {
    console.log(`안녕? 난 차야`);
  }

  Car.defineProperty(Car, 'age'){
    get: function () {
      return this.carAge
    },
    set: function () {
      this.carAge = value
    }
  }
  return Car;
  })()

1.3.3 책 정리 + 주관적인 정리


🔖 책 정리

  • 다른 객체지향언어와 비슷한 수준으로 JS의 클래스도 여러가지 기능을 제공한다.
  • 클래스 이해를 통해 클래스 컴포넌트에 생명주기 구현 및 상속, 함수 표현 방식에 따른 차이를 이해할 수 있다.

🏷️ 주관적인 정리

  • 클래스 컴포넌트는 예전에 리액트 라이프 사이클을 공부할 때 잠깐 스쳐지나간 기억이 있는데, 이런 식으로 리액트 전반에 관련된 중요한 개념인 줄 몰랐다.
  • 현재 우리가 사용하는 함수형 컴포넌트는 결국 클래스 컴포넌트와 동일한 메커니즘이므로, 클래스를 확실하게 이해한다면 함수형 컴포넌트 작성에도 매우 용이할 듯 하다.
  • 더불어 JS 딥다이브에서 공부했던 프로토타입에 관해 다시 정리할 수 있어 좋았다.

1.4 클로저

클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합이다.

🔖 함수형 컴포넌트 작동 방식, 구조, 훅, 의존성 배열 등이 모두 클로저에 의존한다.

1.4.1 클로저의 정의


🔖 클로저는 this와 달리 코드가 작성된 순ㄴ간에 정적으로 결정되는 변수나 함수의 스코프를 사용하는 기법이다.

function add() {
  const a = 10;
  function innerAdd() {
    const b = 20;
    console.log(a + b); // 상위 스코프인 add()에 a 변수가 있기 때문에 가능
  }
  innerAdd();
}

add();

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


🔖 JS의 다양한 스코프를 알아보자!

  • 전역 스코프

    • 전역 레벨에 선언하는 스코프
    • 해당 스코프 선언 시 어디서든 호출 가능하다.
  • 함수 스코프

    • {}블록이 아닌 함수 레벨로 범위를 결정
    • JS는 변수 호출 시 상위 스코프로 변수 탐색 범위를 점점 넓힌다.

1.4.3 클로저의 활용


🔖 환경을 기억하고 이를 사용하는 특징을 이용해 상태 유지나 정보 은닉에 사용

const increase = (function () {
  let num = 0;
  return function () {
    return ++num;
  };
})();

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • 위 함수의 num이 전역 변수였다면 어디서든 활용되었을 것이다.
  • 하지만 함수 안에 넣고, 클로저를 활용해 num의 상태를 유지하면서 함수 안에서만 사용 가능하도록 변경하였다.

🔖 리액트에서의 사용

  • 대표적으로 활용되는 것이 리액트의 useState 훅이다.
function Component () {
  const [num, setNum] = useState();

  function handleClick () {
    ...
    setState((prevNum) => prevNum + 1); // 클로저를 이용해 사용 시의 환경을 기억
  }
}

1.4.4 주의할 점


🔖 기본 개념인 함수와 함수가 선언된 렉시컬 환경의 조합을 항상 인지하고 사용

❗ 꼭 필요한 작업만 남겨두지 않으면 메모리를 불필요하게 소모

❗ 적절한 스코프로 제한하지 않으면 성능에 악영향을 끼침

// 좋지 않은 예시인 긴 작업을 클로저로 처리하는 경우
function heavyJobWithClosure() {
  const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1);
  return function () {
    console.log(longArr.length);
  };
}
const innerFunc = heavyJobWithClosure();
bBtn.addEventListener("click", function () {
  innerFunc();
});
  • 위의 경우 내부 함수가 선언 환경을 기억해야 하므로 이러한 긴 작업 또한 메모리에 올리게 된다.

1.4.5 책 정리 + 주관적인 정리


🔖 책 정리

  • 클로저는 함수형 프로그래밍의 주요 개념인 부수 효과를 줄이고 순수해야 한다라는 목적 달성을 위해 적극 사용되는 개념이다.
  • 하지만 공짜는 아니므로 항상 사용에 주의할 필요가 있다.

🏷️ 주관적인 정리

  • 클로저 자체는 JS 딥다이브 스터디 때 했던 발표로 어느정도 익숙한 개념이었지만 리액트에서 어떻게 이를 사용하는지도 알 수 있어 유익했다.
  • 클로저의 자체적인 활용과 더불어 사용하는 비용에 대한 문제도 고민해볼 수 있었다. (아껴야 잘 산다!)

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

자바스크립트는 싱글 스레드 언어이고, 비동기 작업은 자바스크립트를 멀티 스레드 언어처럼 동작하게 해주는 것이다.

🔖 비동기를 이용해 여러가지 작업을 한번에 처리한다.

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


🔖 스레드에 대한 이해

  • 프로그램의 상태가 메모리상에서 실행되는 작업 단위인 프로세스보다 작은 실행 단위
    • 하나의 프로세스는 여러개의 스레드를 만들 수 있다
    • 스레드끼리 메모리를 공유하여 여러 작업을 동시에 수행 가능하다.

🔖 자바스크립트는 단순히 웹 조작을 보조하기 위한 용도로 만들어져 효율적인 싱글 스레드 언어로 구현되었다.

  • 자바스크립트 코드는 하나의 스레드에서 순차적으로 실행
    • 코드를 한 줄씩 실행한다.
    • 하나의 작업이 끝나기 전까지 뒤의 작업이 시작되지 않는다.
    • Run-to-completion, 동기식으로 순차 처리된다.

1.5.2 이벤트 루프란?


🔖 자바스크립트 런타임 외부에서 비동기 실행을 돕기 위한 장치

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

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

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

foo();
  • 콜 스택 작업 예시
    • 각각의 함수들은 순서대로 콜스택에 들어가고, 실행 후 콜스택에서 제거된다.
    • 기본적인 스택의 LIFO 방식에 따라, baz() -> bar() -> foo() 순으로 제거된다.

✅ 콜 스택이 비어있는지 검사하는 것이 바로 이벤트 루프이다.

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

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

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

foo();
  • 콜스택과 이벤트 루프의 비동기 처리
    • 마찬가지로 차례대로 콜 스택에 들어간다.
    • 하지만 setTimeout 때문에, bar()는 태스크 큐로 들어가고 콜 스택에서 제거된다.
    • 다른 함수들이 실행되고 콜 스택이 비워진게 확인되면 이벤트 루프가 bar()를 콜 스택에 집어넣는다.
    • 함수들이 실행되고 콜 스택이 전부 비워진다.

🔖 이러한 비동기 처리 작업은 자바스크립트의 메인 스레드가 아닌, 태스크 큐가 할당되는 별도의 스레드에서 수행된다.
🔖 자바스크립트 자체 코드 처리를 제외한 외부 Web API는 브라우저나 Node.js가 비동기 처리한다.

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


🔖 이벤트 루프는 하나의 마이크로 태스크 큐를 가지며, 이는 기존의 태스크 큐보다 실행이 우선된다.

  • 태스크 큐 : setTimeout, setInterval, setImmediate
  • 마이크로 태스크 큐 : Promise, MutationObserver, etc..

✅ 각 마이크로 태스크 큐 작업이 종료될 때마다 한번 렌더링할 기회를 얻는다.

  • requestAnimationFrame()을 사용한 예시
console.log("a");

setTimeout(() => {
  console.log("b");
});

Promise.resolve().then(() => {
  console.log("c");
});

window.requestAnimationFrame(() => {
  console.log("d");
});
  • 위 코드의 실행 순서는 a - c - d - b 이다.
  • 따라서 브라우저에 렌더링하는 작업은 마이크로 태스크 큐와 태스크 큐 사이에서 일어난다.

1.5.4 책 정리 + 주관적인 정리


🔖 책 정리

  • 자바스크립트의 싱글 스레드만으로는 불가능한 비동기 처리를 브라우저와 Node.js, 이벤트 루프와 마이크로 태스크 및 태스크 큐들을 통해 가능케 한다.

🏷️ 주관적인 정리

  • 이벤트 루프 자체의 개념은 어렴풋이 알고 있었지만 조금 더 자세하게 이를 들여다보고 각각의 비동기 작업들을 비교하며 알아볼 수 있어 도움이 됐다.
  • 더불어 이벤트 루프와 비동기 처리를 더욱 자세히 알아볼 수 있는 계기가 됐다. 👍

link - 자바스크립트 이벤트 루프 구조와 동작 원리