React 스터디 2주차 클래스형 컴포넌트와 함수형 컴포넌트

Yunes·2023년 8월 13일
0

리액트스터디

목록 보기
5/18
post-thumbnail

📚 서론

이번엔 원래 리액트의 라이프사이클에 대해 조사를 해보려 했다. 그런데 리액트 16.8 버전 이전까지는 라이프사이클이 클래스형 컴포넌트에서만 사용할 수 있었고 그 이후로는 Hook 이 추가되며 useEffect 훅 등을 사용하여 함수형 컴포넌트에서도 비슷한 동작을 구현할 수 있다는 것을 알게 되었다.

현재 react docs 에서도 클래스형 컴포넌트가 여전히 React 에서 지원되고 있으나 새로운 코드를 짤때 이 클래스형 컴포넌트를 사용하라고 권장하지 않는다고 한다.

w3schools react class 에서도 클래스형 컴포넌트를 소개하는 페이지 상단에 마음편히 이 페이지를 생략하고 함수형 컴포넌트를 쓰라고 한다.

그래도 클래스형 컴포넌트와 함수형 컴포넌트의 차이에 대해서는 알아야겠다는 생각이 들어서 이번 포스트를 정리하게 되었다.

📘 JavaScript Class

생각해보면 자바스크립트의 객체나 클래스같은걸 정리해본 적이 없는 것 같다.

클래스형 컴포넌트를 설명하기에 앞서 클래스형 컴포넌트는 JS 의 클래스와 같은데 여기에 React.Component 를 상속받고 render() 메서드를 필요로 하는 점이 특징이다. 그렇다면 자바스크립트의 클래스가 무엇인지 알아야 겠다는 생각이 들었다.

📗 class

클래스는 객체를 생성하는 템플릿이다. 클래스는 데이터와 이걸 조작하는 코드를 캡슐화한다. 함수를 함수 선언식과 함수 표현식으로 정의할수 있듯이 클래스도 클래스 표현식과 선언식으로 정의할 수 있다.

함수 선언식

function calcRectArea(width, height) {
  return width * height;
}

함수 표현식

const getRectArea = function (width, height) {
  return width * height;
};

클래스 선언식

class Polygon {
  constructor(height, width) {
    this.area = height * width;
  }
}

클래스 표현식

const Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  area() {
    return this.height * this.width;
  }
};

함수 선언식과 클래스 선언식의 차이는 호이스팅의 유무이다. 함수 선언식의 위치와 상관없이 호이스팅되어 함수는 선언 이전에 사용할 수 있으나 클래스는 반드시 정의한 뒤에 사용할 수 있다.

  • 클래스 표현식도 클래스 선언식처럼 호이스팅 제한이 걸린다.

📗 메서드

📕 Constructor 생성자

constructor 메서드는 class 로 생성된 객체를 생성하고 초기화하기 위한 메서드이다. 클래스 내에서는 하나만 존재할 수 있다. 부모 클래스의 constructor 를 호출하기 위해 super 키워드를 사용한다.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // 프로토타입 메서드
  calcArea() {
    return this.height * this.width;
  }
  
  // 정적 메서드
  static calcArea(_height, _width) {
  	return _height * _width;
  }
}

const square = new Rectangle(10, 10); 
// new 키워드로 클래스를 통해 squeare 라는 인스턴스 생성
// 이 인스턴스는 프로토타입 체이닝에 의해 프로토타입 메서드를 사용할 수 있다.
// 정적 메서드는 인스턴스 생성 없이 
// Rectangle.calcArea(10,20); 과 같이 사용할 수 있다.

console.log(squeare.calcArea());

프로토타입 채이닝, 프로토타입 메서드, 정적 메서드 등에 관한 설명은 이전 포스트 를 참고하자.

음.. 개인적으로는 백번 말로 떠드는 것보다 한번 직접 겪어보는게 배움에 더 도움이 된다고 생각한다.

그럼 직접 해볼까?

이렇게 실습할때 외부 API 도 데이터베이스도 쓰지 않고 console.log 만 해볼것 같으면 그냥 quokka 플러그인 쓰자. 실시간으로 출력결과가 바로 떠서 매번 실행할 필요 없다.

클래스에서 constructor, 프로토타입 메서드, 정적 메서드의 차이는 알아야 한다고 생각한다.

클래스 내에서 정의된 함수는 프로토타입 메서드이고 static 을 붙이면 정적 메서드라고 알고 있는데 진짜 그럴까?

new 키워드로 생성된 인스턴스가 프로토타입 체이닝을 통해 area() 를 사용할 수 있으니 클래스 내에서 정의된 메서드는 프로토타입 메서드이다.

정적 메서드는 클래스 내에서 정의되었으나 static 이 붙는 메서드이며 인스턴스 생성 없이 클래스명을 통해 바로 사용할 수 있다.

생성자 함수를 사용하여 객체를 생성하면 객체가 생성될 때마다 생성자 함수 안에 정의된 함수가 새로 정의되고 할당된다.

class Rectangle {
	constructor(height, width) {
    	this.heigth = height;
      this.width = width;
    }
}

Rectangle.prototype.sayHi = function () {
  console.log('Hi');
  console.log(this);
}

const rectangle = new Rectangle(5, 4);
rectangle.sayHi();

이걸 보면 프로토타입 메서드 내의 this 는 해당 클래스 자체를 나타내는 것을 알 수 있다. 이렇게 할수는 있지만 JavaScript AirBnb style guide 를 보면 class 구문이 간결하고 의미를 알기 쉽기에 prototype 을 직접 조작하는 것을 피하라고 한다. 대신 클래스 내에서 직접 함수를 정의하는 방식을 사용하자.

this 를 사용하지 않는 메서드는 정적 메서드로 정의하는 것이 좋다. (정적 메서드는 메서드 내부에서 클래스를 호출한 객체의 프로퍼티에 접근할 수 없다.)

자바스크립트는 constructor overload 를 허용하지 않는다.

public class MyClass {
    private int value;

    // 매개변수 없는 생성자
    public MyClass() {
        value = 0;
    }

    // 정수형 매개변수를 받는 생성자
    public MyClass(int v) {
        value = v;
    }

    // 두 개의 정수형 매개변수를 받는 생성자
    public MyClass(int v1, int v2) {
        value = v1 + v2;
    }

    public static void main(String[] args) {
    }
}

자바같은 언어는 같은 이름의 생성자 함수가 있어도 파라미터의 타입과 개수가 다르면 각기 다르게 동작하도록 오버로드가 가능하다.

그런데 자바스크립트는 동적언어다! 타입이 다른지 같은지 체크를 엄격히 하지 않는다. 그래서 일반적으로는 자바스크립트에서 오버로드를 할 수 없다.

비슷하게 동작하고 싶다면 오버로드 자체는 안되고 그 내부에서 분기처리를 따로 해줘야 한다.

📗 extends, super

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // super class 생성자를 호출하여 name 매개변수 전달
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

let d = new Dog("Mitzie");
d.speak(); // Mitzie barks.

개는 동물을 상속받는다. 이때 생성자함수에 super 를 사용할 수 있는데 constructor 내에서 super 는 최상단에서 사용해야 한다. 이 super 는 상속한 부모 클래스의 constructor 를 의미한다.

자바스크립트는 오버로딩은 없지만 오버라이딩은 제공한다.

부모 클래스에서 정의한 메서드를 자식 클래스에서도 정의할 경우 자식 메서드가 부모 메서드를 덮어씌워 오버라이딩 한다.

즉, super(name) 은 Animal 의 constructor 에 name 을 전달했고 인스턴스 d 가 d.speak() 으로 호출한 메서드 speak() 는 부모 클래스의 speak() 를 실행하려다 자식 클래스의 speak() 가 오버라이딩 하여 Mitzie barks 가 출력된 것이다. 단, 기본적으로 부모 클래스의 메서드를 호출한다.

📕 is a 개념과 has a 개념

is a : 상속

클래스들 사이의 포함 관계를 의미하며 한 클래스가 다른 클래스의 서브클래스임을 의미한다.

// 부모 클래스 정의
class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log(`${this.name}가 소리를 내었다.`);
    }
}

// "is a" 관계: Dog는 Animal의 하위 클래스.
class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }

    // Dog 특정 메서드
    fetch() {
        console.log(`${this.name}가 공을 가져왔다.`);
    }
}

// Dog 인스턴스 생성
const myDog = new Dog("맥스", "골든 리트리버");

// 메서드 호출
myDog.speak();  // 출력: 맥스가 소리를 내었다.
myDog.fetch();  // 출력: 맥스가 공을 가져왔다.

Dog is a kind of Animal 관계 성립

has a : 객체 합성

한 오브젝트가 다른 오브젝트에 속하는 관계를 말한다.

// "has a" 관계: Car는 Engine을 가지고 있다.
class Engine {
    start() {
        console.log("엔진을 시작한다.");
    }
}

class Car {
    constructor(make, model) {
        this.make = make;
        this.model = model;
        this.engine = new Engine();
    }

    drive() {
        console.log(`${this.make} ${this.model}가 달린다.`);
    }

    startEngine() {
        this.engine.start();
    }
}

// Car 인스턴스 생성
const myCar = new Car("현대", "소나타");

// 메서드 호출
myCar.drive();        // 출력: 현대 소나타가 달린다.
myCar.startEngine();  // 출력: 엔진을 시작한다.

Car has an Engine 관계 성립

이제야 클래스형 컴포넌트를 알아볼 준비가 된 것 같다.

📘 Component

클래스형 컴포넌트는 React.Component 를 상속받는 클래스이다. 이 컴포넌트는 render() 메서드를 필요로 하고 이 메서드는 HTML 을 반환한다.

react.dev Class Component 예제

class Greeting extends Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

클래스형 컴포넌트를 사용하고 싶다면 built-in 클래스인 Component 를 상속받고 render() 메서드를 정의해야 한다.

클래스형 컴포넌트도 클래스이기 때문에 constructor 가 안보여도 비어있는 constructor() {} 가 있는 상태라고 보면 된다. 이 생성자 함수는 컴포넌트가 초기화될때 호출된다. 생성자함수를 통해 컴포넌트의 프로퍼티를 초기화할 수 있다.

class Car extends React.Component {
  constructor() {
    super();
    this.state = {color: "red"};
  }
  render() {
    return <h2>I am a Car!</h2>;
  }
}

react 에서 constructor 는 state 를 선언하는 것과 클래스 메서드를 클래스 인스턴스에 바인딩 시키는 역할을 한다.

앞에서 알아본 것처럼 super(); 는 상속한 부모 클래스의 생성자함수를 호출하는 것인데 이때의 부모클래스는 React.Component 가 된다.

리액트에서 컴포넌트 프로퍼티는 state 라 불리는 객체로 관리되어야 한다.

혹은 컴포넌트 프로퍼티는 props 를 통해 다룰 수 있는데 props 는 속성으로서 컴포넌트에 보낼 수 있고 함수의 인자와 같다. 이때 this.props 를 통해 사용할 수 있다.

클래스형 컴포넌트에서의 statethis.state 를 통해 사용할 수 있고 상태는 직접 변경하는게 아니라 setState 를 통해 새 상태를 호출한다.

class Greeting extends Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

<Greeting name="Taylor" />
class Counter extends Component {
  state = {
    age: 42,
  };

  handleAgeChange = () => {
    this.setState({
      age: this.state.age + 1 
    });
  };

  render() {
    return (
      <>
        <button onClick={this.handleAgeChange}>
        Increment age
        </button>
        <p>You are {this.state.age}.</p>
      </>
    );
  }
}

볼수록 함수형 컴포넌트가 클래스형 컴포넌트와 매우 닮았다는 생각이 든다.

클래스형 컴포넌트의 state 는 함수형 컴포넌트의 useState 와 같다.

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // ...
  }

constructor 에서 super(props) 를 다른 statement 보다 먼저 호출하지 않으면 this.props 는 undefined 가 된다.

이건 당연할 수도 있긴 한데 state 는 constructor 에서만 직접 할당할 수 있고 그 외의 메서드에선 setState 를 사용해야 한다.

setState 는 생성자 함수에서 호출하면 안된다.

📗 라이프 사이클

📕 Mount - 초기화 단계

순서
constructor - getDerivedStateFromProps - render - componentDidMount

constructor : 컴포넌트 클래스의 생성자 함수, 컴포넌트를 만들때 처음으로 호출되는 함수이고 state 초기값 할당, 메서드와 컴포넌트 바인딩

getDerivedStateFromProps : props 와 state 값 동기화, DOM 의 요소노드들을 렌더링하기 전에 호출된다. 정적 메서드인 this 객체에 접근할 수 없다.

class Header extends React.Component {
  constructor(props) {
    super(props);
    this.state = {favoritecolor: "red"};
  }
  static getDerivedStateFromProps(props, state) {
    return {favoritecolor: props.favcol };
  }
  render() {
    return (
      <h1>My Favorite Color is {this.state.favoritecolor}</h1>
    );
  }
}

render : 컴포넌트의 기능과 모양새 정의, 클래스형 컴포넌트에서 반드시 구현되어야 한다.
componentDidMount : 컴포넌트를 생성하고 첫 렌더링이 끝났을 때 호출되는 함수. DOM 에서 컴포넌트가 모두 렌더링되었을때 실행한다.

class ChatRoom extends Component {
  state = {
    serverUrl: 'https://localhost:1234'
  };

  componentDidMount() {
    this.setupConnection();
  }

  componentDidUpdate(prevProps, prevState) {
    if (
      this.props.roomId !== prevProps.roomId ||
      this.state.serverUrl !== prevState.serverUrl
    ) {
      this.destroyConnection();
      this.setupConnection();
    }
  }

  componentWillUnmount() {
    this.destroyConnection();
  }

  // ...
}

리액트는 componentDidMount() 를 컴포넌트가 화면에 초기화 (추가) 될때 호출한다. 따라서 구독, 데이터 fetching, DOM 노드 조작할때 사용된다.

componentDidMount() 가 어떤 state 나 props 를 읽었다면 반드시 그 변화들을 다루기 위해 componentDidUpdatecomponentDidMount() 가 수행중이던 모든 작업을 정리하기 위해 componentWillUnmount 를 사용해야 한다.

📕 Update - 업데이트 단계

컴포넌트가 업데이트되는 경우는 props 값 변경, state 값 변경, 부모 컴포넌트가 리렌더링 될 때, this.forceUpdate로 강제로 리렌더링 되는 경우가 있다.

순서
getDerivedStateFromProps - shouldComponentUpdate - render - getSnapshotBeforeUpdate - componentDidUpdate

getDerivedStateFromProps : 위와 동일
shouldComponentUpdate : 컴포넌트를 리렌더링 할지 말지를 결정하는 함수다. true를 반환하면 아래 함수들을 호출하여 업데이트에 따른 리렌더링을 진행하며 false를 반환할경우 리렌더링을 하지 않고 아래 함수도 실행되지 않는다.

render : 새로운 값을 사용하여 View 를 리렌더링 한다.
getSnapshotBeforeUpdate : 변경된 요소에 대하여 DOM 객체에 반영하기 직전에 호출되는 함수다. 이 함수를 실행중이라면 반드시 componentDidUpdate 메서드도 포함해야 한다.

class Header extends React.Component {
  constructor(props) {
    super(props);
    this.state = {favoritecolor: "red"};
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({favoritecolor: "yellow"})
    }, 1000)
  }
  getSnapshotBeforeUpdate(prevProps, prevState) {
    document.getElementById("div1").innerHTML =
    "Before the update, the favorite was " + prevState.favoritecolor;
  }
  componentDidUpdate() {
    document.getElementById("div2").innerHTML =
    "The updated favorite is " + this.state.favoritecolor;
  }
  render() {
    return (
      <div>
        <h1>My Favorite Color is {this.state.favoritecolor}</h1>
        <div id="div1"></div>
        <div id="div2"></div>
      </div>
    );
  }
}

componentDidUpdate : 컴포넌트 업데이트 작업이 끝난 리렌더링 후에 호출되는 함수다. 갱신이 일어난 직후에 호출되므로 최초 렌더링시에 호출되지 않는다. 이전과 현재의 props 를 비교하여 네트워크 요청을 보내는 것도 이 단계에서 이루어진다.

componentDidUpdate 를 정의하면 React 는 컴포넌트가 재렌더링되거나 props 나 state 를 변경한 직후에 호출한다. 이때 setState() 를 즉시 호출할 수 있으나 조건문으로 감싸지 않으면 무한반복이 발생할 수 있다. 부모 컴포넌트에서 내려온 props 를 그대로 state 에 저장하는 것은 좋지 않고 대신 prop 을 직접 사용하는 것 이 좋다.

class ChatRoom extends Component {
  state = {
    serverUrl: 'https://localhost:1234'
  };

  componentDidMount() {
    this.setupConnection();
  }

  componentDidUpdate(prevProps, prevState) {
    if (
      this.props.roomId !== prevProps.roomId ||
      this.state.serverUrl !== prevState.serverUrl
    ) {
      this.destroyConnection();
      this.setupConnection();
    }
  }

  componentWillUnmount() {
    this.destroyConnection();
  }

  // ...
}

📕 Unmount - 소멸 단계

소멸될 때 수행되는 과정이다.

componentWillUnmount() : 마운트가 해제되어 제거되기 직전에 호출된다. 이때 타이머 제거, 네트워크 요청 취소, 구독권 해제 등의 정리 작업이 이뤄진다.

또한 이제 컴포넌트가 재렌더링 되지 않으니 setState 를 호출해선 안된다.

componentWillUnmount() 가 정의되었다면 React 는 컴포넌트가 제거되기 전에 이 함수를 호출한다. componentDidMount 의 동작과 반대되는 동작을 한다.

class Child extends React.Component {
  componentWillUnmount() {
    alert("The component named Header is about to be unmounted.");
  }
  render() {
    return (
      <h1>Hello World!</h1>
    );
  }
}

📗 함수형 컴포넌트 vs 클래스형 컴포넌트

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}
function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

클래스형 컴포넌트는 상태값을 가질 수 있고 리액트 컴포넌트의 생명주기를 작성할 수 있다. 함수형 컴포넌트와의 차이는 상태값과 생명주기를 가질수 있는지 여부인데 리엑트 16.8 버전부터 Hook 의 등장으로 함수형 컴포넌트에서도 상태값과 생명주기함수를 작성할 수 있게 되었다.

  • 클래스형 컴포넌트에서의 state 는 객체 형식이다.

📔 레퍼런스

docs
react.dev - component
mdn - JavaScript/Classes
reference
벨로퍼트와 함께하는 모던 리액트
w3school react class
airbnb javascript style guide
blog
sdc337dc - 클래스형과 함수형 차이
seong-dodo 클래스형 컴포넌트와 함수형 컴포넌트
kwonh - 함수형, 클래스형 컴포넌트

📓 결론

사실 클래스형 컴포넌트와 함수형 컴포넌트의 비교에 대한 것을 조사하는걸 미뤄왔다 공식 문서에서도 함수형 컴포넌트의 사용을 권장하기도 하고 클래스형 컴포넌트에 대한 중요성이 낮게 느껴졌었기 때문이다. 그래도 한번쯤은 정리하고 넘어가야 한다는 생각이 들어 이번포스트를 정리하게 되었다.

그런데 막상 작성해보고 나니 함수형 컴포넌트와 클래스형 컴포넌트의 비교보다 자바스크립트의 클래스에 대해 더 자세히 알아본 느낌이 든다..

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글