그동안 개발하면서 쉬운 사용성 때문에 주로 함수형 컴포넌트을 사용해왔었는데 문득 함수형 컴포넌트만을 사용해도 괜찮을까? 하는 의문점이 들었다.
클래스형 컴포넌트
- 리액트 16.8버전(2019년 2월) 이전까지 활발히 사용되던 컴포넌트
- 컴포넌트를 선언할 때 Class 문법 사용
- 리액트에서 제공되는 Component 클래스를 상속하여 생성
클래스형 컴포넌트의 구조
- class를 사용하여 컴포넌트를 정의하며 render()를 통해 컴포넌트를 렌더링 한다.
- extends로 만들고 싶은 컴포넌트를 extends 해야한다.
- React.Component
- React.PureComponent
import React from "react";
class MyComponent extends React.Component {
// constructor에서 props를 넘겨주고 state의 기본값을 설정한다.
private constructor(props) {
super(props);
this.state = {
count: 0,
isLimited: false,
};
}
// render 내부에서 쓰일 함수를 선언한다.
private handleClick = () => {
const newValue = this.state.count + 1;
this.setState({ count: newValue, isLimited: newValue >= 10 });
};
// render에서 이 컴포넌트가 렌더링할 내용을 정의한다.
public render() {
// props와 state 값을 this, 즉 해당 클래스에서 꺼낸다.
const {
props: { required, text },
state: { count, isLimited },
} = this;
return (
<h2>
Sample Component
<div>{required ? "필수" : "필수 아님"}</div>
<div>문자 : {text}</div>
<div>count : {count}</div>
<button onClick={this.handleClick} disabled={isLimited}>
증가
</button>
</h2>
);
}
}
- constructor()
- 컴포넌트 내부에 이 생성자 함수가 있다면 컴포넌트가 초기화되는 시점에 호출된다.
- 위의 코드에서는 컴포넌트의 state를 초기화 한다.
- super()는 컴포넌트를 만들면서 상속받은 상위 컴포넌트인 React.Component를 먼저 호출해 해당 클래스가 초기화될 때 부모 클래스의 기능을 사용할 수 있도록 한다.
- props
- 함수에 인수를 넣는 것과 비슷하게 컴포넌트에 특정 속성을 전달하는 용도로 쓰인다.
- state
- 클래스 컴포넌트 내부에서 관리하는 값을 의미한다.
- 항상 객체여야 하며 값의 변화가 있을 때마다 리렌더링 된다.
- 메서드
- 렌더링 함수 내부에서 사용되는 함수
- 보통 DOM에서 발생하는 이벤트와 함께 사용된다.
- 만드는 방식은 크게 3가지로 나뉜다.
- constructor에서 this 바인드를 하는 방법 일반적인 함수로 메서드를 만들면 this가 전역 객체가 바인딩되기 때문에 strict로 인해 undefined로 나온다. 따라서 생성된 함수에 bind를 활용해 강제로 this를 바인딩 해야한다.
- 화살표 함수를 쓰는 방법 this가 상위 스코프로 결정되는 화살표 함수를 사용하면 굳이 바인딩을 하지 않아도 된다.
- 렌더링 함수 내부에서 함수를 새롭게 만들어 전달하는 방법 간편하지만 렌더링이 일어날 때마다 새로운 함수를 생성해서 할당하므로 최적화를 수행하기 어려워진다.
// constructor에서 this 바인드를 하는 방법
private constructor(props){
this.handleClick = this.handleClick.bind(this)
}
// 화살표 함수를 쓰는 방법
private handleClick = () => {
const newValue = this.state.count + 1;
this.setState({ count: newValue, isLimited: newValue >= 10 });
};
// 렌더링 함수 내부에서 함수를 새롭게 만들어서 전달하는 방법
<button onClick={() => this.handleClick()}>증가</button>
클래스 컴포넌트의 생명주기 메서드

-
마운트(mount) 컴포넌트가 마운팅(생성) 되는 시점
- render() : UI를 렌더링함 this.setState 호출 금지
- componentDidMount() : 마운트 되는 즉시 실행, this.setState가 가능하나 웬만하면 지양
-
업데이트(update) : 이미 생성된 컴포넌트의 내용이 변경(업데이트) 되는 시점
- render()
- componentDidUpdate() : DOM을 업데이트하는 등에 쓰임 this.setState가 가능하나 업데이트 될 때마다 호출되기 때문에 적절한 조건문이 필요
-
언마운트(unmount) : 컴포넌트가 더 이상 존재하지 않는 시점
- componentWillUnmount() : 컴포넌트가 언마운트되거나 더이상 호출하지 않을 때 사용하며 this.setState를 호출하지 못함
클래스 컴포넌트의 한계
- 데이터의 흐름을 추적하기 어려움
- 서로 다른 메서드에서 state의 업데이트가 일어난다.
- 코드를 읽는 과정에서 state가 어떤 식의 흐름으로 변경되서 렌더링이 일어나는지 판단하기 어렵다.
- 기능이 많아질수록 컴포넌트의 크기가 커진다.
- 컴포넌트 내부에 로직이 많아질수록 데이터 흐름이 복잡해서 생명 주기 메서드 사용이 잦아지는 경우 컴포넌트의 크기가 기하급수적으로 커진다.
- 클래스는 함수에 비해 상대적으로 어렵다.
- 자바스크립트는 프로토타입 기반의 언어라 클래스는 비교적 뒤에 나온 개념
- 클래스보다는 함수에 익숙하며 자바스크립트 환경에서는 함수보다 클래스의 사용이 어렵고 일반적이지 않다.
함수 컴포넌트
- 리액트16.8버전 이전에는 단순한 무상태 컴포넌트를 구현하기 위한 수단
- 훅(Hook)의 등장으로 무상태 컴포넌트에 상태를 더할 수 있게 되었다.
- 클래스 컴포넌트와 비교했을 때 간결한 코드와 좋은 사용성
// 클래스형 컴포넌트
import React, { Component } from 'react';
type SampleProps = {
required?: boolean;
text: string;
}
type SampleState = {
count: number;
isLimited: boolean;
}
class SampleComponent extends Component<SampleProps, SampleState> {
constructor(props: SampleProps) {
super(props);
this.state = {
count: 0,
isLimited: false
};
}
handleClick = () => {
const newValue = this.state.count + 1;
this.setState({
count: newValue,
isLimited: newValue >= 10
});
}
render() {
const { required, text } = this.props;
const { count, isLimited } = this.state;
return (
<h2>
Sample Component
<div>{required ? '필수' : '필수 아님'}</div>
<div>문자 : {text}</div>
<div>count : {count}</div>
<button onClick={this.handleClick} disabled={isLimited}>
증가
</button>
</h2>
);
}
}
export default SampleComponent;
//함수형 컴포넌트
import { useState } from 'react'
type SampleProps = }
required? : boolean
text: string
}
export function SampleComponent({ required, text } : SampleProps) {
const [count, setCount] = useState<number>(0)
const [isLimited, setLimited] = useState<boolean>(false)
function handleClick() {
const newValue = count + 1
setCount(newValue)
setIsLimited(newValue >= 10)
}
return (
<h2>
Sample Component
<div>{required ? '필수' : '필수 아님'}</div>
<div>문자 : {text}</div>
<div>count : {count}</div>
<button onClick={handleClick} disabled={isLimited}>
증가
</button>
</h2>
)
}
함수 컴포넌트 vs 클래스 컴포넌트
- 생명주기 메서드의 부재
- 생명주기 메서드는 React.Component에서 오기 때문에 이를 상속받아서 구현하는 클래스 컴포넌트에서만 생명주기 메서드가 사용가능하다.
- 함수 컴포넌트에서는 useEffect 훅을 사용해 componentDidMount, componentDidUpdate...등을 비슷하게 구현할 수 있다.
- 함수 컴포넌트와 렌더링된 값
- 함수 컴포넌트는 props와 state가 변경된다면 다시 한 번 그 값을 기준으로 함수가 호출된다.
- 클래스 컴포넌트는 시간의 흐름에 따라 변화하는 this를 기준으로 렌더링이 일어난다.
클래스 컴포넌트를 공부해야 할까?
- 클래스 컴포넌트가 당장 사라질 계획은 없다.
- 시간이 많다면 기존의 클래스형 컴포넌트를 함수형 컴포넌트로 변경하는 것은 고려해볼 만 하지만 단순히 코드를 옮기는 것 이상의 세심한 주의가 필요하다.
- 예전의 코드를 유지보수하는데 도움이 된다.
- 이전 리액트 16.8버전에서는 클래스 컴포넌트가 활발히 사용되었기 때문에 기존 리액트 코드를 리팩토링하고 유지보수 하는데에는 많은 도움이 될 수 있다.
마치며
처음 함수 컴포넌트만 사용해도 괜찮은가 라는 질문에는 Yes라고 대답할 수 있을 것 같다. 하지만 숙련된 리액트 개발자가 되려면 그동안 리액트가 어떠한 고민을 통해 발전했는지 이해할 필요가 있다. 클래스 컴포넌트에 대해 배워둔다면 리액트를 매끄럽게 다루는 데에 큰 도움이 될 수 있을 것 같다.