-
Notifications
You must be signed in to change notification settings - Fork 2
Description
더 좋은 가독성 => https://www.notion.so/370c728328c548ebb9cd459006cb6817
리액트에서 컴포넌트를 구현할 때에는 두 가지 방식이 있다.
바로, 클래스형 컴포넌트와 함수 컴포넌트이다.
대부분 리액트를 처음 접하게 되면 함수 컴포넌트가 권장된다는 사실에 큰 고민없이 함수 컴포넌트를 사용하는 사람들이 많을 것으로 생각된다.
검색을 해보면 간결한 구조, 상태로직의 재활용, Hooks의 도입과 같은 이유로 권장된다고 하는데, 잘 와닿지 않으니 조금 더 깊게 공부해보려 한다.
Hooks의 도입?
함수 컴포넌트와 비교하여 클래스형 컴포넌트는 생명주기 관련 메서드라는 아주 강력한 기능을 가지고 있었다.
현재는 hooks의 도입으로 이러한 기능이 대부분 대체되고 있어 함수 컴포넌트의 사용을 권장하는데 먼저 이에 대해 알아보자.
-
생명주기..?
리액트의 생명주기에는 다양한 단계가 있지만 크게 마운트, 업데이트, 언마운트로 나눌 수 있다.
- 마운트(mount) : 컴포넌트가 생성되는 시점
- 업데이트(update) : 컴포넌트 내용이 변경되는 시점
- 언마운트(unmount) : 컴포넌트가 삭제되어 더 이상 존재하지 않는 시점
어라..? 익숙하지 않은가?
마운트 시점에 특정 로직을 수행하고 싶을 때 함수 컴포넌트에서는 어떻게 처리했을까?
바로 useEffect를 사용해왔다!
두 번째 인자로 빈 배열을 두어 컴포넌트가 생성되는 시점 한 번만 시행되도록 할 수 있었다.
업데이트 시점에도 useEffect의 두 번째 인자로 state를 설정하면, state가 변경되어 컴포넌트의 내용이 변경되는 시점에 특정 로직을 수행할 수 있었다.
마지막으로 언마운트는 이번 timer 과제를 하면서 useEffect의 return () ⇒ clearInterval 를 사용해보았을 텐데, 이는 업데이트 단계에서 clean up을 수행해주는 용도도 있지만 컴포넌트가 언마운트 되었을 때도 실행된다!
그렇다. 우린 알게 모르게 리액트의 주요 생명주기 시점에 대한 로직 처리를 잘 수행하고 있었다!
클래스형 컴포넌트에서 사용할 수 있는 주요 생명주기 관련 메서드는 다음과 같다.
- render() : ui 렌더링
- componentDidMount() : 첫 마운트 시점에 호출
- componentDidUpdate() : 컴포넌트 업데이트 후 호출
- componentWillUnmount() : 컴포넌트가 언마운트 되기 직전 호출
render() 함수는 함수 컴포넌트에서 return으로 jsx 코드를 랜더링 할 수 있으므로 불필요하며,
나머지 메서드는 모두 함수 컴포넌트에선 useEffect로 대체가능한 메서드이다!
또한, useEffect의 두번째 인자의 state를 추적하여 업데이트를 해주는 함수 컴포넌트와는 달리
클래스 컴포넌트의 componentDidUpdate는 파라미터로 이전 state와 props 값을 가져와
현재 state와 비교해주는 과정을 거쳐야 하기에 Hooks의 편리함이 드러나는 부분도 쉽게 찾아볼 수 있다.
클래스형 컴포넌트 업데이트
class MyComponent extends Component {
state = {
count: 0,
name: 'John',
};
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
// 추가 작업 수행
}
if (prevState.name !== this.state.name) {
// 추가 작업 수행
}
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment Count</button>
<p>Name: {this.state.name}</p>
<button onClick={this.changeName}>Change Name</button>
</div>
);
}
}
export default MyComponent;함수 컴포넌트 업데이트
useEffect(() => {
// 추가 작업 수행
}, [count]); // count가 변경될 때마다 실행
useEffect(() => {
// 추가 작업 수행
}, [name]); // name이 변경될 때마다 실행결론적으로, 함수 컴포넌트에서는 대부분의 클래스에서 사용되는 생명주기 관련 메서드들을 구현할 수 있으며 오히려 더 나은 가독성과 사용법을 가질 수 있다!
상태로직의 재사용?
함수 컴포넌트가 클래스형 컴포넌트에 비해 상태로직의 재사용이 유리하다고 한다.
먼저 함수 컴포넌트의 재사용 예시 코드를 봐보자
함수형 컴포넌트
import React, { useState, useEffect } from 'react';
// 커스텀 훅
function useFetchData(url) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => {
setData(data);
});
}, [url]);
return data;
}
// 사용 예시
function MyComponent() {
const data = useFetchData('https://api.example.com/data');
return data ? <div>Data: {data}</div> : <p>Loading...</p>;
}
function AnotherComponent() {
const data = useFetchData('https://api.example.com/other-data');
return data ? <div>Other Data: {data}</div> : <p>Loading...</p>;
}
export default function App() {
return (
<div>
<MyComponent />
<AnotherComponent />
</div>
);
}되게 간단한 코드이지만 코드 울렁증이 있는 분들을 위해 간단히 설명을 하자면,
특정 url을 받아 데이터를 요청해 반환 받은 데이터를 반환해주는 훅과 그 훅을 사용하는 두 가지의 컴포넌트를 구현한 코드이다.
우리가 자연스럽게 사용해왔듯이, 함수 컴포넌트에서는 커스텀 훅을 사용하여 자주 사용되는 상태 관련 로직 등을 재사용하였다.
클래스형 컴포넌트
import React, { Component } from 'react';
// HOC
function withFetchData(url, WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
data: null,
};
}
componentDidMount() {
fetch(url)
.then((response) => response.json())
.then((data) => {
this.setState({ data });
})
}
render() {
const { data } = this.state;
return <WrappedComponent data={data} {...this.props} />;
}
};
}
// 클래스형 컴포넌트: MyComponent
class MyComponent extends Component {
render() {
const { data } = this.props;
return data ? <div>Data: {data}</div> : <p>Loading...</p>;
}
}
// 클래스형 컴포넌트: AnotherComponent
class AnotherComponent extends Component {
render() {
const { data } = this.props;
return data ? <div>Other Data: {data}</div> : <p>Loading...</p>;
}
}
// HOC로 컴포넌트를 감싸기
const MyComponentWithData = withFetchData('https://api.example.com/data', MyComponent);
const AnotherComponentWithData = withFetchData('https://api.example.com/other-data', AnotherComponent);
// 메인 App 컴포넌트
export default class App extends Component {
render() {
return (
<div>
<MyComponentWithData />
<AnotherComponentWithData />
</div>
);
}
}클래스형 컴포넌트는 HOC라는 다소 복잡한 방법을 사용해야 한다.
코드를 보면 알 수 있듯이, withFetchData 함수의 return으로 class형 컴포넌트를 반환하는 것을 볼 수 있고 그 안에 fetch 메서드와 인자로 받은 컴포넌트를 감싸서 출력하는 것을 볼 수 있다.
이처럼 함수 컴포넌트의 custom hook을 통해 상태관리 로직을 훨씬 깔끔하고 편리하게 사용할 수 있다는 장점을 확인 할 수 있다!
this의 부작용
위의 클래스형 컴포넌트 예제를 보면 알 수 있듯이 클래스형 컴포넌트에서는 this를 사용하여 state에 접근한다.
-
this 가 뭐임?
자바스크립트나 객체지향 프로그래밍에 안 익숙한 사람들은 this 키워드가 다소 낯설 수 있다.
https://jeong-pro.tistory.com/79 이 글을 읽어보는 것을 추천한다.
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}버튼을 눌렀을 때, 3초 후에 해당 유저에 대한 팔로우 메세지를 alert 해주는 코드가 있다.
같은 코드같지만, 클래스형 컴포넌트에는 치명적인 오류가 발생할 수 있다.
this는 가변적이다.
따라서 버튼을 누른 시점의 this는 해당 컴포넌트를 가르키고 있지만,
3초 사이에 컴포넌트가 리랜더링 될 때, this는 변경된 컴포넌트를 가르키게 된다.
따라서 다른 user에 대한 정보가 출력될 수 있는 오류가 존재한다.
(함수 컴포넌트에서는 props는 당연히 절대적인 값이므로 변경되지 않고 올바르게 출력된다.)
물론 클래스형 컴포넌트에서도 버튼을 누른 시점의 유저에 대한 스냅샷을 찍어 출력할 수 있지만 번거로우며 클로저와 같은 어려운 개념도 숙지하여야 한다.
결론
클래스형 컴포넌트에 비해 함수 컴포넌트가 가지는 이점이 생각보다 많은 것으로 판단된다.
이는 함수 컴포넌트 방식에 익숙해진 탓이 크겠지만, 괜히 hooks의 도입으로 새로운 패러다임을 이끌어가는 게 아니라고 느낄 정도로 간결하고 직관적인 기능을 제공하는 이점이 많다고 느껴진다.
다만, 클래스형 컴포넌트 방식도 어느정도 불편함은 있지만 객체지향 프로그래밍에 익숙한 사람이면 더욱 선호될 수도 있고 앞서 말한 단점들도 큰 어려움 없이 해결 가능하기 때문에 개발 환경에 따라 능동적으로 선택할 수 있는 역량을 기르는 것이 좋을 것 같다.
참고글 :
https://overreacted.io/how-are-function-components-different-from-classes/
https://react.vlpt.us/basic/24-class-component.html
