[React.js] 'Class Component' Vs 'Functional Component'
React Component에는 Class형과, Function형 두 가지가 존재한다.
두 컴포넌트를 코드를 이용해서 분석해보자.
1. Rendering JSX
JSX란?
JSX란 JavaScript XML의 약자로, JavaScript를 확장한 문법이다.
React에서는 이벤트가 처리되는 방식, 시간에 따라 state가 변하는 방식, 화면에 표시하기 위해 데이터가 준비되는 방식 등 렌더링 조직이 본질적으로 다른 UI 로직과 연결된다는 사실을 받아들인다.
React는 별도의 파일에 마크업과 로직을 넣어 기술을 인위적으로 분리하는 대신, 둘 다 포함하는 "컴포넌트"라고 부르는 느슨하게 연결된 유닛으로 관심사를 분리한다.
React는 JSX 사용이 필수가 아니지만, 대부분의 사람은 JavaScript 코드 안에서 UI 관련 작업을 할 대 시각적으로 더 도움이 된다고 생각한다. 또한 React가 더욱 도움이 되는 에러 및 경고 메시지를 표시할 수 있게 해준다.
JSX는 Component의 종류에 따라 구문이 달라진다. 아래의 예시를 참고한다.
Functional Component에서는 아래와 같이 return을 이용해 JSX를 반환한다.
import React from "react";
const FunctionalComponent = () => {
return <h1>Hello, world</h1>;
};
반면에 Class Component에서는 render 메서드 내에서 JSX를 반환한다.
import React, { Component } from "react";
class ClassComponent extends Component {
render() {
return <h1>Hello, world</h1>;
}
}
2. props 전달
Component 생성 시 부모 Component로부터 값을 받아 올 수 있는데, React에서는 이를 props 인자로 전달한다.
부모 Component에서 자식 Component 호출 시 아래와 같이 name 변수에 "shiori"라는 값을 전달하는 경우를 예로 들자.
<Component name="Shiori" />
Functional Component에서는 아래와 같이 Parameter를 이용하여 값을 받아온다.
const FunctionalComponent = ({ name }) => {
return <h1>Hello, {name}</h1>;
};
const FunctionalComponent = (props) => {
return <h1>Hello, {props.name}</h1>;
};
Class Component에서는 Class형이므로 this를 사용해야한다.
class ClassComponent extends React.Component {
render() {
const name = this.props.name;
return <h1>Hello, { name }</h1>;
}
}
3. Handling state
state는 최근까지 Class Component에서만 가능한 기능이었다.
하지만 React 16.8부터 Function Component에서도 state를 사용할 수 있도록 React Hook useState가 도입되었다.
0부터 시작하는 간단한 카운터를 만들고 버튼을 클릭할 때마다 1씩 증가하는 예제를 구현해본다.
Handling state in Class Components
먼저 Class Component에서는 constructor를 이용해 state을 선언 및 초기화 한다. constructor의 정의는 아래와 같다.
"React Component의 constructor는 mount 되기 전에 호출된다. React.Component 하위 클래스에 대한 constructor를 구현할 때 다른 구문보다 먼저 super(props);를 호출해야한다. 그렇지 않으면 this.props가 생성자에서 정의되지 않아 버그가 발생할 수 있다."
기본적으로 생성자를 구현하고 super(props)를 호출하지 않으면 사용하려는 모든 state 변수가 정의되지 않는다.
또한 constructor 내부에서 this.state를 이용해서 state 변수를 정의할 수 있으며, constructor 내부에서는 sestState()를 호출하면 안된다. (constructor는 this.state를 직접 할당할 수 있는 유일한 곳)
constructor 외부에서 state 변수값을 변경할 때는 this.state로 직접 접근이 아닌, this.setState 메서드를 이용해야 한다.
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
Handling state in Functional Components
React 16.8 부터는 Functional Component에서 Hook 기능을 이용해 state 핸들링이 가능해졌다.
Hook란 Functional Component에서 React state와 생명주기(Lifecycle) 기능을 연동, 연결해주는 함수이다.
Hook을 사용해 기존의 Class Component의 문제점을 해결할 수 있으며, Class Component의 문제점은 아래와 같다.
Class Component의 단점
- Class Compoent에서 로직 재사용 시에 사용하는 고차 컴포넌트, render는 React Component 트리를 깊게 만든다. 따라서 성능에 부정적인 영향과 개발 시 디버깅이 힘들어지는 문제가 발생한다.
- 서로 연관없는 로직들을 하나의 생명주기에 작성해야하는 경우가 발생한다.
- componentDidMount와 componentWillUnmount, comopnentDidMount와 componentDidUpdate처럼 반복해서 작성해야 하는 코드가 발생한다.
- 컴퓨터 입장에서도 클래스 사용 시 코드 압축이 잘 안되는 경우, 핫 리로드에서 난해한 버그가 발생하며 컴파일 단계에서 코드 최적화 어려움이 발생한다.
- JS의 클래스 문법, this에 대한 이해가 필요해, React의 진입장벽을 높힌다.
Hook의 장점
- 여러 Hook들끼리 재조립이 가능하므로, 재사용 가능한 로직을 쉽게 만들 수 있다.
- 로직을 한 군데에 모을 수 있다.
- Hook은 단순한 함수이기 때문에 정적 타입 언어에서도 타입을 쉽게 작성할 수 있다.
이와같은 이유로 Hooks를 사용하면 Class없이 React의 기능을 더 많이 사용가능하며, 개념적으로 React Component는 항상 함수에 더 가깝다.
Hook은 기본적으로 State Hook과 Effect Hook이 존재하며, State Hook에서는 state 관리를 Effect Hook에서는 Effect 발생을 위해 사용된다. 그 외에도 사용자 정의 Hook 생성이 가능하다. (자세한 내용은 공식문서 참고)
useState(init_value)를 호출하면 init_value를 초기값으로 가지는 state, setState를 순서대로 담은 배열을 반환한다.
아래 코드는 앞에서 다루었던 Class Component와 같은 동작을 하지만, 훨씬 코드가 단축된 것을 확인할 수 있다.
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect()는 아래의 Lifecycle에서 마저 다뤄본다.
4. Lifecycle Methods
LifeCycle은 렌더링 타이밍에서 중요한 역할을 한다. Class Component에서 Function Component로 마이그레이션하려는 사용자는 useEffect() 사용을 통해 Class Component의 componentDidMount()를 대체할 수 있다.
On Mounting(componentDidMount)
LifeCycle 메서드 componentDidMount는 첫 번째 렌더링이 완료된 직후 호출되지만, 어떤 값이 변경됐을 경우에 실행되도록 할 수도 있다. useEffect로 전달된 함수는 레이아웃 배치, 화면 그리기가 완료될 때까지 지연된 뒤 실행한다.
대부분의 작업이 렌더링을 차단해서는 안되지만, 사용자가 UI데이터와 화면의 내용 불일치를 경험하지 않도록 동기적으로 Effect를 발생시키고 싶다면 useLayoutEffect 함수를 이용한다.
useLayoutEffect의 내부 코드는 브라우저가 화면을 그리기 이전 시점에 동기적으로 수행된다.
useEffect는 두번째 매개변수로 의존성 배열을 받는다.
useEffect Hook에서 API 통신을 하면 렌더링할 때 마다 호출되기 때문에 불필요하게 많이 동작하게 되는데, 두 번째 매개변수로 배열을 입력하면, 배열(두 번째 매개변수)의 값이 변경되는 경우에만 함수가 호출된다.
const FunctionalComponent = () => {
React.useEffect(() => {
console.log("Hello");
}, []);
return <h1>Hello, World</h1>;
};
아래는 같은 기능을 수행하는 Class Component의 예로 이해를 돕기위해 추가한 코드이다.
class ClassComponent extends React.Component {
componentDidMount() {
console.log("Hello");
}
render() {
return <h1>Hello, World</h1>;
}
}
On Unmounting (componentWillUnmount)
useEffect() 메서드 내부에서 return을 이용해 해제하는 함수를 등록함으로써 Event Mount 해제가 가능하다.
즉, useEffect Hook 이용 시 Mounting과 Unmounting 관리를 같이 할 수 있다.
const FunctionalComponent = () => {
React.useEffect(() => {
return () => {
console.log("Bye");
};
}, []);
return <h1>Bye, World</h1>;
};
두 스타일 모두 장단점이 있지만, React 16.8 이후부터는 Functional Component에 Hook이라는 기능을 지원함으로써 좀 더 간결하게 작성되어 개발, 이해 및 테스트가 더 용이해졌다. 따라서 Class Component보다는 Functional Component를 더 권장하는 분위기이다.
참고 자료 :