클래스 컴포넌트
- 기존 리액트 16.8 버전 미만으로 작성된 코드로 클래스 컴포넌트가 대다수이다. 즉, React의 초기 버전부터 사용되어 온 전통적인 방식으로, 복잡한 상태관리와 생명주기 관리가 필요한 경우 유리할 수 있다.
- 생명주기 메소드(LifeCycle Method) : 컴포넌트가 브라우저상에 나타나고, 업데이트 되고, 없어지게 될 때, 또는 추가적으로 에러가 났을 때 호출되는 메소드들이다. 생명주기 메소드는 클래스 컴포넌트에서만 사용할 수 있고, 예를들어 componentDidMount(), componentDidUpdate(), componentWillUnmount(), render() 등이 있다.
- 클래스 컴포넌트를 알아야하는 이유 : 지금은 함수 컴포넌트 사용을 권장하기 때문에 알 필요 없다고 생각할 수도 있으나, 현재까지 많이 쓰이고 있는 다양한 수 많은 라이브러리들은 클래스 컴포넌트 기반으로 만들어진 게 대부분이기 때문에 이해를 하고 넘어가야할 필요가 있다.
// Constructor.tsx
import React from "react";
// props 타입 정의
// title은 선택적 프로퍼티로, title뒤에 ?를 통해 string 타입이거나 제공하지 않아도 됨
interface ComponentProps {
title?: string;
}
// state의 타입 정의
// count는 필수 프로퍼티로, number 타입으로 지정
interface ComponentState {
count: number;
}
// 컴포넌트에서 <propsType, stateType>를 상속받는 클래스를 정의
// 이때 propsType과 stateType에는 앞서 정의한 interface를 지정
class Constructor extends React.Component<ComponentProps, ComponentState> {
// 컴포넌트의 생성자 함수
// props를 인자로 받고, super()를 호출하여 부모 클래스의 생성자 함수를 실행
// state를 초기화하며, count의 기본값을 0으로 설정
constructor(props: ComponentProps) {
super(props); // super 메소드로 react 컴포넌트의 생성자를 호출
this.state = {
count: 0, // 처음에 렌더링 될 count 상태 초기값을 설정
};
}
// count 상태를 5 감소시키는 메소드
decrement = (): void => {
this.setState({ count: this.state.count - 5 });
};
// count 상태를 10 증가시키는 메소드
// state의 this 바인딩을 사용하지 않고 화살표 함수로 작성
increment = (): void => {
this.setState((prev) => ({ count: prev.count + 10 }));
};
// 컴포넌트의 렌더링을 담당하는 생명주기 메소드
// jsx를 반환하여 화면에 렌더링 될 내용을 정의
render() {
return (
<div>
<h1>props의 title : {this.props.title}</h1>
<p>상태가 변경될 때마다 렌더링되어 보여지는 요소: {this.state.count}</p>
<button onClick={this.decrement}>-5 감소</button>
<button onClick={this.increment}>+10 증가</button>
</div>
);
}
}
export default Constructor;
// App.tsx
import Constructor from "./Constructor";
function App() {
return (
<div className="App">
{/* Constructor 컴포넌트를 렌더링하고, title prop에 값을 전달 */}
<Constructor title="컴포넌트 제목" />
</div>
);
}
export default App;
작성한 예제 코드에서 핵심 내용은 props, state, 메소드이다. 타입스크립트를 사용하여 타입 안정성이 보장된 리액트 클래스 컴포넌트의 기본적인 구조를 보여준다. props와 state의 타입을 명시적으로 정의함으로써, 컴포넌트의 예상 동작을 더 명확히 하고, 개발 과정에서 발생할 수 있는 타입스크립트 관련 오류를 사전에 방지할 수 있다. 또한 동기적으로 컴포넌트의 상태 관리 및 이벤트 핸들링 방법을 보여주며, JSX 렌더링을 사용한 UI의 선언적 정의 방식을 예시로 든 코드이다.
클래스 컴포넌트의 생명주기 메소드
이미지 출처 :
React Lifecycle Methods diagram
render()
- react 클래스 컴포넌트의 유일한 필수 값으로 항상 쓰이며, 컴포넌트가 UI를 렌더링하기 위해서 쓰이는 가장 중요한 메소드
- 같은 props와 state에 대해 항상 동일한 결과를 반환해야하며, 부작용을 발생시키지 않아야함
- 다른 컴포넌트를 render() 메소드 내에서 호출함으로써, UI의 재사용 가능한 부분을 구성할 수 있음
- 조건부 렌더링을 사용할 수 있음. 특정 조건에 따라 다른 컴포넌트나 요소를 렌더링할 수도 있음.
componentDidMount()
- 클래스 컴포넌트가 마운트되고 준비되는 즉시 실행.
- API 호출을 할 때 용이하다. 컴포넌트가 화면에 나타난 직후 데이터를 불러와야 할 때, 예를 들어 서버로부터 데이터를 요청하고 이를 컴포넌트의 상태에 저장
- 컴포넌트가 마운트 된 후에 DOM을 직접 조작해야하는 경우, 예를 들어 스크롤 위치 설정이나 외부 라이브러리를 사용하여 DOM 요소에 애니메이션을 추가하는 경우에 사용
- 특정 이벤트에 대한 리스너를 설정하고 싶을 때, 이 메소드 내에서 이벤트 리스너를 추가할 수 있음
import React from 'react;
class ThisIsComponent extends React.Component {
componentDidMount(prevProps, prevState) {
// fetch 함수를 사용하여 API 호출
fetch('<https://api.example.com/items>')
.then(response => response.json())
.then(data => this.setState({ items: data }));
// DOM 조작 예시
document.title = '새로운 타이틀';
// 이벤트 리스너 설정
window.addEventListener('resize', this.handleResize);
}
}
export default ThisIsComponent;
componentDidUpdate()
- 컴포넌트 업데이트가 일어난 이후 바로 실행
- 여기서 this.setState를 사용하면 매 순간마다 fetchData()가 실행되는 불상사를 막기위해 if 조건문으로 제어하는 것이 좋다.
import React from 'react;
class ThisIsComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
// props의 특정 값이 변경되었는지 확인
if (this.props.username !== prevProps.username) {
this.fetchData(this.props.username);
}
// state의 특정 값이 변경되었는지 확인
if (this.state.count !== prevState.count) {
// 새로운 count 값에 따라 뭔가를 실행
}
}
fetchData(username) {
// username에 따라 사용자 데이터를 불러오는 로직
return this.props.username;
}
}
export default ThisIsComponent;
componentWillUnmount()
- 컴포넌트가 언마운트 되거나 더 이상 사용되지 않기 직전에 호출됨.
- API 호출을 취소하거나, setInterval, setTimeout으로 생성된 타이머를 지우는 등 작업을 하는 데 유용한 메소드
import React from 'react';
class ThisIsComponent extends React.Component {
componentDidMount() {
// fetch 함수를 사용하여 API 호출
fetch('<https://api.example.com/items>')
.then(response => response.json())
.then(data => this.setState({ items: data }));
// DOM 조작 예시
document.title = '새로운 타이틀';
// 이벤트 리스너 설정
window.addEventListener('resize', this.handleResize);
}
// 이벤트 리스너 제거를 위한 componentWillUnmount() 호출
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
}
export default ThisIsComponent;
shouldComponentUpdate()
- 특정한 성능 최적화 상황에서 고려해야하며, 불필요한 렌더링을 방지할 수 있음
- 기본적으로 this.setState가 한번 호출 될 때마다 컴포넌트는 리렌더링을 일으키는데, state나 props의 변경으로 컴포넌트가 리렌더링 되는 것을 막을 때 사용
- 제어문을 사용하여 리렌더링을 해야할지 말아야할지 결정해줄 수 있음
import React from 'react';
interface ComponentProps {
title?: string;
}
interface ComponentState {
count: number;
}
class ThisIsComponent extends React.Component<ComponentProps, ComponentState> {
state: MyComponentState = {
count: 0,
};
shouldComponentUpdate(nextProps: ComponentProps, nextState: ComponentState): boolean {
// 현재 count와 다음 state의 count를 비교
if (this.state.count !== nextState.count) {
return true; // count가 변경되었으면 컴포넌트를 업데이트
}
return false; // 그 외의 경우에는 업데이트하지 않음
}
incrementCount = (): void => {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
};
render() {
return (
<div>
<h1>{this.props.title}</h1>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment</button>
</div>
);
}
}
export default ThisIsComponent;
getDerivedStateFromProps()
- render() 를 호출하기 직전에 호출, 새로운 props를 받게 될 때마다 호출
- 이 메소드는 static(정적)으로 선언되어 있어서 this에 접근 불가
import React from 'react';
interface ComponentProps {
color: string;
}
interface ComponentState {
backgroundColor: string;
}
class ThisIsComponent extends React.Component<ComponentProps, ComponentState> {
constructor(props: ComponentProps) {
super(props);
this.state = {
backgroundColor: 'white', // 초기 상태 설정
};
}
// 정적인 getDerivedStateFromProps 메소드 정의
static getDerivedStateFromProps(props: ComponentProps, state: ComponentState): ComponentState | null {
// props로 받은 color가 현재 상태와 다른 경우, 상태를 업데이트
if (props.color !== state.backgroundColor) {
return {
backgroundColor: props.color, // 새로운 상태 반환
};
}
// 상태를 변경할 필요가 없는 경우, null을 반환
return null;
}
render() {
return (
<div style={{ backgroundColor: this.state.backgroundColor }}>
Background color: {this.state.backgroundColor}
</div>
);
}
}
export default ThisIsComponent;
getSnapShotBeforeUpdate()
- React 16.3 이전에 사용 되었던 생명 주기인 componentWillUpdate()를 대체할 수 있는 메소드
componentWillUpdate() 메소드는 부수 효과를 발생시키거나 setState를 호출하면 안되는 제약이 있었고, 이러한 사용이 권장 되지 않았음
- componentDidUpdate()가 호출 되기 바로 직전에 호출, 즉 DOM 변경이 화면에 반영되기 바로 전에 DOM 상태를 저장하기 위해 사용될 수 있었음
- 스크롤 위치를 유지하거나, DOM의 특정 측정값을 저장하는 데 유용
import React from 'react';
interface ComponentProps {
someValue: number;
}
interface ComponentState {
items: string[]; // 스크롤 가능한 아이템 리스트를 상태 타입 정의
}
class ThisIsComponent extends React.Component<ComponentProps, ComponentState> {
container: HTMLDivElement | null = null; // 스크롤 컨테이너 DOM에 대한 참조를 저장
constructor(props: ComponentProps) {
super(props);
this.state = {
items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'], // 초기 아이템 리스트
};
}
getSnapshotBeforeUpdate(prevProps: ComponentProps, prevState: ComponentState): any {
// 컴포넌트가 업데이트되기 전에 현재 스크롤 위치를 저장
if (this.container) {
return this.container.scrollHeight - this.container.scrollTop;
}
return null;
}
componentDidUpdate(prevProps: ComponentProps, prevState: ComponentState, snapshot: any) {
// 업데이트가 완료된 후, 이전 스크롤 위치를 복원
if (snapshot !== null && this.container) {
this.container.scrollTop = this.container.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={el => this.container = el} style={{ overflow: 'auto', height: '200px' }}>
{this.state.items.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
}
}
export default ThisIsComponent;
클래스 컴포넌트의 한계
1. 데이터 흐름을 추적하기 어렵다
여러 메소드에서 state의 업데이트가 가능하고, 생명주기 메소드의 순서에 따라 코드를 작성하지 않아도 되기 때문에, 코드를 읽으며 state의 흐름을 추적하기가 어렵다.
2. 애플리케이션 내부 로직의 재사용이 어렵다
고차 컴포넌트(Higher Order Component)로 감싸거나 props를 넘겨주는 방식으로 로직을 재사용하려 하지만, 이는 래퍼 지옥(wrapper hell)으로 이어질 위험이 있고, 상속을 사용하는 경우 코드의 복잡도가 증가한다.
3. 기능이 많아질수록 컴포넌트 크기가 커진다
내부 로직과 데이터 흐름이 복잡해짐에 따라 생명주기 메소드의 사용이 잦아지고, 이는 컴포넌트의 크기를 기하급수적으로 증가시킨다.
4. 클래스는 함수에 비해 상대적으로 어렵다
자바스크립트는 프로토타입 기반 언어이기 때문에, 많은 개발자들이 함수에 더 익숙하며, 클래스 사용이 상대적으로 어렵다.
5. 코드 크기를 최적화 하기 어렵다
클래스 컴포넌트는 최종 결과물인 번들 크기 최적화에 어려움을 겪는다.
6. 핫 리로딩을 하는 데 상대적으로 불리하다
코드 변경 시 앱을 다시 시작하지 않고 변경 사항을 적용하는 핫 리로딩에 있어 클래스 컴포넌트는 함수형 컴포넌트에 비해 불리하다.
함수 컴포넌트
클래스 컴포넌트와 비교했을 때, 함수 컴포넌트는 더 간결하고 명료하며 사용하기 쉬운 특징을 가지고있다. 최근 리액트 개발 트렌드에서는 함수 컴포넌트와 React Hooks를 사용하여 상태 관리와 생명주기 기능을 구현하는 방식을 선호한다
간결함
- 클래스 컴포넌트에 비해서 더 간결하고 이해하기 쉬운 코드를 작성할 수 있음
- return 문을 사용해 JSX를 반환하기만 하면 되기 때문에, 간단한 표현으로 UI를 묘사할 수 있음
Hooks 사용
- React 16.8 버전에서 도입된 Hooks를 통해 함수 컴포넌트 내에서도 상태 관리와 같은 리액트의 기능을 사용할 수 있음
- useState, useEffect와 같은 내장 Hooks를 사용하여 클래스 컴포넌트에서만 가능했던 상태 관리와 생명주기 관련 로직을 처리할 수 있음
재사용성과 조합성
- 커스텀 Hooks를 만들어 사용할 수 있고 로직을 쉽게 재사용하고 조합할 수 있음
- 코드의 중복을 줄이고 테스트하기 쉬운 코드를 작성하는데 많은 도움을 줌
핫 리로딩과의 호환성
- 핫 리로딩과의 호환성이 좋아 개발 과정에서 코드 변경 사항을 빠르게 확인 가능
성능적 이점
- 클래스 컴포넌트에 비해서 메모리 자원을 적게 사용하는 경향이 있음
- 버전이 올라갈 수록 React 팀에서 코드를 최적화 하는 데 많은 노력을 기울이고 있음
- this 바인딩을 조심할 필요 없음
- state는 객체가 아닌 각각의 원시 값으로 관리되어 훨씬 사용하기가 편함
함수 컴포넌트 vs 클래스 컴포넌트
생명주기 메소드의 유무
- 클래스 컴포넌트는 React의 React.Component 클래스를 상속받아 만들어진다. 이 클래스는 여러 생명주기 메소드를 제공하는데, 이 메소드들은 컴포넌트가 생성되고 업데이트 되고 제거되는 등의 다양한 시점에서 자동으로 호출된다.
- 함수 컴포넌트에는 단순히 props를 받아서 React 요소를 반환하는 함수이다. 이러한 구조때문에 생명주기 메소드를 직접 가질 수 없다. 그럼에도 불구하고 useEffect 훅으로 앞서 언급했던 클래스 컴포넌트의 생명주기 메소드인 componentDidMount, componentDidUpdate, componentWillUnmount등을 비슷하게 구현할 수 있다
- useEffect는 생명주기 메소드와 똑같이 동작하지는 않지만, 컴포넌트가 렌더링될 때마다 특정 작업을 수행할 수 있도록 해준다. 예를 들어, 컴포넌트가 화면에 처음 나타날 때만 어떤 작업을 하고 싶다면, useEffect의 두 번째 인자로 빈 배열을 전달함으로써 componentDidMount와 유사한 효과를 낼 수 있다. 또한, useEffect 내에서 반환하는 함수는 컴포넌트가 화면에서 사라질 때 실행되므로, componentWillUnmount와 유사한 역할을 한다.
💡 요약: useEffect는 함수 컴포넌트에서 생명주기 메소드와 같은 역할을 할 수 있게 해주지만, 생명주기 메소드 자체는 아니다. 대신 컴포넌트가 업데이트될 때마다 특정 작업을 수행하거나, 컴포넌트가 처음 나타나거나 사라질 때 작업을 실행하는 방식으로 사용. 이를 통해 함수 컴포넌트에서도 복잡한 상태 관리와 부수 효과를 다룰 수 있게 된다.
클래스 컴포넌트를 공부해야하는지?
맨 처음에 언급했던 것 처럼 클래스 컴포넌트를 공부해야할 이유는 존재한다. 현재까지 리액트 생태계 내에 이미 방대한 양의 클래스 컴포넌트가 만들어졌을 것이고, 많은 프로젝트, 라이브러리, 기존의 코드 베이스들이 클래스 컴포넌트를 사용하여 구축되었다. 이러한 코드들이 단순히 존재하는 것 뿐만 아니라, 여전히 활발히 사용되고 있으며, 때로는 유지보수나 업데이트가 필요한 경우도 많다. 그래서 이를 모두 함수 컴포넌트의 훅 기반으로 모두 이관하기는 불가능에 가깝다.
결론적으로는 함수 컴포넌트 hooks 기반으로 먼저 코드를 작성해 보면서 리액트를 익히고, 어느정도 적응이 되었다 싶으면 이후에 클래스 컴포넌트까지 익혀본다면 리액트를 매끄럽게 다루는 데에 큰 문제가 없을 것이다.
참고 자료
https://www.yes24.com/Product/Goods/123161563
모던 리액트 Deep Dive - 예스24
요즘 프런트엔드 개발은 자바스크립트와 리액트부터 시작한다는 말이 있을 정도로 최근 몇 년간 프런트엔드 생태계에서 리액트의 비중은 날이 갈수록 커지고 있습니다. 이 책에서는 0.x 버전의
www.yes24.com
https://overreacted.io/how-are-function-components-different-from-classes/
How Are Function Components Different from Classes? — overreacted
How do React function components differ from React classes? For a while, the canonical answer has been that classes provide access to more features (like state). With Hooks, that’s not true anymore. Maybe you’ve heard one of them is better for performa
overreacted.io
'Frontend > React' 카테고리의 다른 글
[React] 프로젝트에 eslint 적용 시 발생한 이슈 (0) | 2024.04.01 |
---|---|
[React] Hooks 기초 정리/가장 많이 쓰이는 hooks (0) | 2024.01.31 |