함수 컴포넌트는 리액트 0.14 버전부터 만들어진 꽤 역사가 싶은 컴포넌트 선언 방식이다.
var Aquarium = (props) -> {
var fish = getFish(props.species)
return <Tank>{fish}</Tank>
}
var Aquarium = ({species}) => <Tank>{getFish(species)}</Tank>
이때 당시의 함수 컴포넌트는 클래스 컴포넌트에서 생명주기 메서드나 상태가 필요없이 render만 하는 경우에만 제한적으로 사용됐다. 하지만 함수형 컴포넌트에 Hook이 등장하며 복잡한 클래스 컴포넌트보다 함수 컴포넌트를 더 많이 쓰기 시작했다.
클래스 컴포넌트
기존 리액트 16.8 미만으로 작성된 코드에는 클래스 컴포넌트가 대다수일 것이다.
var Aquarium = (props) -> {
var fish = getFish(props.species)
return <Tank>{fish}</Tank>
}
var Aquarium = ({species}) => <Tank>{getFish(species)}</Tank>
클래스 컴포넌트를 만들려면 클래스를 선언하고 extends로 만들고 싶은 컴포넌트를 extends해야 한다.
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
console.log('컴포넌트 마운트');
}
componentDidUpdate(prevProps, prevState) {
console.log('컴포넌트 업데이트');
if (prevState.count !== this.state.count) {
console.log('count 값 변경');
}
}
componentWillUnmount() {
console.log('컴포넌트 언마운트');
}
handleIncrement = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
render() {
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.handleIncrement}>Increase</button>
</div>
);
}
}
export default MyComponent;
생명주기
클래스 컴포넌트를 사용하며 자주 언급되는 것이 바로 생명주기다. 생명주기 메서드가 실행되는 시점은 크게 3가지로 나눌 수 있다.
- 마운트(mount): 컴포넌트가 마운팅(생성)되는 시점
- 업데이트(update): 이미 생성된 컴포넌트의 내용이 변경(업데이트)되는 시점
- 언마운트(unmount): 컴포넌트가 더 이상 존재하지 않는 시점
render()
컴포넌트가 UI를 렌더링하기 위해 사용하며 마운트와 업데이트 과정에서 일어난다.
해당 메서드는 항상 순수해야 하며 부수 효과가 없어야 한다. 따라서 입력값(props혹은 state)이 들어가면 항상 같은 결과물을 반환해야 한다. 즉, 내부에서 state를 직접 업데이트하는 this.setState를 호출해서는 안된다.
componentDidMount()
클래스 컴폰너트가 마운트되고 준비가 됐다면 그 다음으로 호출되는 생명주기 메서드이다. 내부에서는 this.setState()로 state 값을 변경하는 것이 가능하다.
일반적으로 state를 다루는 것은 생성자에서 하는 것이 좋으며, 생성자 함수에서 할 수 없는 API 호출 후 업데이트나 DOM에 의존적인 작업 등을 하기 위해서다.
componentDidUpdate()
컴포넌트 업데이트가 일어난 이후 바로 실행되며, state나 props의 변화에 따라 DOM을 업데이트하는 등에 사용된다.
그러나 적절한 조건문으로 감싸지 않으면 this.setState가 계속해 호출되는 일이 발생되어 성능에 악영향을 줄 수 있다.
componentWillUnmount()
컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출되며 메모리 누수, 불필요한 작동을 막기 위한 클린업 함수를 호출한다.
shouldComponentUpdate()
state나 props의 변경으로 리렌더링되는 것을 막고 싶다면 이 생명주기 메서드를 사용하며, 특정한 성능 최적화 상황에서만 고려해야 한다.
Component VS PureComponent
이 두 컴포넌트 유형의 차이는 생명주기를 다루는데 있다.
Component의 경우 버튼을 누르는대로 (state가 업데이트되는대로) 렌더링이 발생하지만,
PureComponent의 경우 state의 값이 업데이트되지 않아서 렌더링이 일어나지 않는다.
import React, { Component } from "react";
class MyComponent extends Component {
state = {
count: 0,
};
handleClick = () => {
this.setState((prevState: any) => ({
count: prevState.count,
}));
};
render() {
console.log("Component 렌더링");
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
export default MyComponent;
import React, { PureComponent } from "react";
class MyComponent extends PureComponent {
state = {
count: 0,
};
handleClick = () => {
this.setState((prevState: any) => ({
count: prevState.count,
}));
};
render() {
console.log("PureComponent 렌더링");
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
export default MyComponent;
PureComponent가 모든 경우에 좋은 것은 아니다. 객체의 얕은 비교를 수행하기 때문에 깊은 객체의 데이터 변화는 감지할 수 없다.
만약 대부분 컴포넌트에서 PureComponent로 구성되어 있지만 state가 객체로 되어있다면 성능 향상에 무의미한 효과를 가질 수 있다.
static getDerivedStateFromProps()
가장 최근에 도입된 생명주기 메서드로 render()를 호출하기 직전에 호출되며 static으로 선언되어 this에 접근할 수 없다는 특징이 있다.
getSnapShotBeforeUpdate()
DOM이 업데이트되기 직전에 호출된다. DOM이 렌더링 되기 전에 윈도우 크기를 조절하거나 스크롤 위치를 조정하는 등의 작업처리에 유용하다.
지금까지 언급한 생명주기 메서드를 다이어그램으로 확인하면 아래와 같은 구조를 갖는다.
getDerivedStateFromError()
에러 상황에서 실행되는 메서드로 자식 컴포넌트에서 에러가 발생했을 때 호출된다.
클래스 컴포넌트의 한계
클래스 컴포넌트에서 제공하는 메서드만으로도 완성도 있는 리액트 애플리케이션을 만드는 데는 충분하다. 그렇다면 함수 컴포넌트의해 클래스 컴포넌트가 밀리는 이유는 무엇일까?
- 데이터의 흐름을 추적하기 힘들다. 서로 다른 메서드에서 state의 업데이트가 일어날 수 있으며 코드 작성시 메서드의 순서가 정해져 있지 않아 사람이 읽기도 어렵다.
- 내부 로직의 재사용이 어렵다. 공통 로직이 많아질수록 이를 감싸는 고차 컴포넌트 내지는 props가 많아지는데 이를 클래스 컴포넌트에서 매끄럽게 처리하기 쉽지 않다.
- 기능이 많아질수록 컴포넌트의 크기가 커지게 된다. 특히 생명주기 메서드의 사용이 잦아지는 경우 그 크기가 기하급수적으로 커진다.
- 클래스는 함수에 비해 상대적으로 어렵다. 또한 JS에서 클래스의 사용은 비교적 어렵고 일반적이지 않기에 개발자들에게 혼란을 안겨줄 수 있다.
- 코드 크기를 최적화하기 어렵다. 최종 결과물인 번들 크기를 줄이는데 어려움을 겪게 된다.
함수 컴포넌트
16.8에서 함수 컴포넌트에서 사용 가능한 Hook이 등장하면서 리액트 개발자들에게 각광받고 있다.
import React, { useState } from 'react';
const MyComponent = () => {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>증가</button>
</div>
);
};
export default MyComponent;
render 내부에서 필요한 함수를 선언할 때 this 바인딩을 조심할 필요도 없으며, state는 객체가 아닌 각각의 원시값으로 관리되어 훨씬 사용하기 편해졌다.
함수 컴포넌트 VS 클래스 컴포넌트
함수 컴포넌트에는 클래스 컴포넌트에 존재하는 생명주기 메서드가 없다. 함수 컴포넌트는 props를 받아 리액트 요소만 반환하기 때문이다. 따라서 함수형 컴포넌트는 useEffect 훅을 이용해 componentDidMount, componentDidUpdate, componentWillUnmount를 비슷하게 구현할 수 있다.
그렇다 하지만 비슷하게일 뿐이지 useEffect는 생명주기를 위한 훅이 아니다. state를 활용해 동기적으로 부수 효과를 만드는 매커니즘이다.
함수 컴포넌트는 렌더링된 값을 고정하고, 클래스 컴포넌트는 렌더링된 값이 고정되지 않는다.
https://overreacted.io/how-are-function-components-different-from-classes/
함수 컴포넌트와 클래스 컴포넌트로 구성된 아래 코드를 보면 두 코드는 같은 방식으로 동작하는 것처럼 보인다.
두 방식 모두 버튼을 누른 경우 3초뒤에 props에 있는 user값을 alert로 알려준다.
함수 컴포넌트: 클릭한 시점의 props값을 기준으로 메시지를 출력
클래스 컴포넌트: alert를 띄우는 시점의 props값을 기준으로 메시지를 출력
클래스 컴포넌트는 props의 값을 항상 this로부터 가져온다. 즉 컴포넌트 인스턴스의 멤버는 변경 가능하기 때문에 이러한 현상이 일어나게된다.
물론 아래와 같은 방법으로 this.props를 조금 일찍 부르고 이를 함수에 인수로 넘기는 방법이 있다.
class ProfilePage extends React.Component {
showMessage = (user) => {
alert('Followed ' + user);
};
handleClick = () => {
const {user} = this.props;
setTimeout(() => this.showMessage(user), 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
하지만 이는 props가 늘어날수록 코드가 복잡해지는 단점이 있다.
혹은 아래처럼 render에 필요한 값을 넣는 방법이 있다. render() 함수가 실행되는 순간에 모든 값을 미리 넣어두는 방법이 있다.
class ProfilePage extends React.Component {
render() {
// Capture the props!
const props = this.props;
// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
하지만 이런 방식을 사용한다면 클래스 컴포넌트 방식과 거리도 멀고 렌더링될 때마다 함수가 다시 생성되고 할당되기 때문에 성능에도 악영향을 미치게된다.
함수 컴포넌트는 props를 인수로 받기 때문에 컴포넌트는 그 값을 변경할 수 없기에 그대로 사용하게 되며 state에도 그대로 적용되게 된다.
클래스 컴포넌트의 미래는?
클래스 컴포넌트는 일단 사라질 계획은 없다. 리액트 커뮤니티에는 많은 수의 클래스 컴포넌트가 만들어져 있으며 이를 모두 바꾸는 것은 어렵다. 따라서 클래스형 컴포넌트를 종료시키는 것은 쉽지 않을 것이다.
'React > 모던 리액트 Deep Dive' 카테고리의 다른 글
리액트의 렌더링과 메모이제이션 (1) | 2024.11.08 |
---|---|
가상 DOM과 리액트 파이버 (2) | 2024.10.30 |
[React Deep Dive] JSX(Javascript XML) (0) | 2024.10.26 |
[React Deep Dive] 타입스크립트 (1) | 2024.10.17 |
[React Deep Dive] 동등 비교 (0) | 2024.10.08 |