
React 16.8v에서 추가된 기능으로 클래스형 컴포넌트에서만 할 수 있었던 React state와 생명주기 기능(lifecycle features) 등을 함수형 컴포넌트에서도 사용할 수 있게 해주는 요소이다.
우선 클래스형 컴포넌트보다 함수형 컴포넌트가 훨씬 간결하고 가독성이 좋다. 하지만 함수형 컴포넌트에서는 State와 Life Cycle을 지원하지 않았기 때문에 이전에는 복잡하고 불편하더라도 클래스형 컴포넌트를 사용할 수 밖에 없었다. 그래서 React는 아래와 같은 기존 클래스형 컴포넌트에서의 불편함을 개선하고 Hooks를 추가하여 함수형 컴포넌트에서 State와 Life Cycle을 사용할 수 있게 했다.
클래스형 컴포넌트는 클래스의 형태를 띄고 있어서 클래스의 구조를 유지하기 위해 코드가 상대적으로 길고 복잡해진다.
예를 들어 클래스형 컴포넌트에서 상태를 관리하기 위해 constructor 메서드를 사용하고 생성자(constructor) 내에서는 super(props)를 호출해야 한다. 이는 부모 클래스의 생성자를 호출하고, props를 전달하는 것이다. React 컴포넌트의 생성자에서 super(props)를 호출하지 않으면 this.props가 정의되지 않아서 문제가 발생할 수 있다.
중요한 점은 매번 같은 코드를 반복 작성해야한다는 것이다.
클래스형 컴포넌트 내에서 이벤트 핸들러를 정의할 때, this를 사용한다. 컨텍스트에 따라 this가 변경되기 때문에 적절히 바인딩을 잘해주어야한다. 이로 인해 코드의 가독성이 떨어지고, 버그가 발생할 가능성이 높아진다. 일반적으로 constructor에서 바인딩을 해주거나, 화살표 함수를 사용하여 메서드를 정의할 때 자동으로 바인딩된다.
클래스형 컴포넌트에서는 자주 쓰는 로직을 빼내어 코드를 재사용하기 위해 HOC를 사용해야 한다. 이를 통해 로직을 재사용하고, 컴포넌트 간의 의존성을 분리할 수 있다. 하지만 HOC를 사용하면 컴포넌트 트리가 깊어지고 가독성이 떨어지는 등의 데이터의 흐름을 파악하기가 힘들다.
클래스형 컴포넌트에서는 생명주기 메서드를 사용하여 컴포넌트의 생명주기에 따른 동작을 정의해야 한다. 즉, 컴포넌트가 마운트, 언마운트, 업데이트될 때마다 호출되는 다양한 메서드를 정의해야 함을 의미한다. 이로 인해 컴포넌트의 로직이 분산되고 이해하기 어려워질 수 있다.
//Class Component
import React, { Component } from 'react';
class ClassComponent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
console.log('Component did mount');
}
componentDidUpdate(prevProps, prevState) {
console.log('Component did update');
if (prevState.count !== this.state.count) {
console.log('Count state updated');
}
}
componentWillUnmount() {
console.log('Component will unmount');
}
incrementCount = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
<div>
<h2>Class Component</h2>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment</button>
</div>
);
}
}
export default ClassComponent;
//Functional Component
import React, { useState, useEffect } from 'react';
const FunctionComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Component did mount');
return () => {
console.log('Component will unmount');
};
}, []);
const incrementCount = () => {
setCount(prevCount => prevCount + 1);
}
return (
<div>
<h2>Function Component</h2>
<p>Count: {count}</p>
<button onClick={incrementCount}>Increment</button>
</div>
);
}
export default FunctionComponent;
결론적으로 위의 예시를 비교했을 때 클래스형 컴포넌트에서의 state와 생명 주기 작성하는 것보다 함수형 컴포넌트에서 Hooks를 사용하여 작성하는 것이 비교적 가독성이 높고 간결하게 작성이 가능한 것을 확인할 수 있다.
React에서 기본적으로 지원하는 hook들이다. 이외에도 다양한 Custom Hooks들이 많다.
① useState : 함수 컴포넌트에서 상태를 추가하고 상태를 업데이트할 때 사용된다.
② useEffect : 컴포넌트가 렌더링될 때마다 특정 작업을 수행하거나, 특정 상태가 변경될 때 특정 작업을 수행할 수 있다.
③ useContext : 컴포넌트 간에 상태를 공유하거나, 전역 상태를 관리할 때 사용된다.
④ useReducer : useState보다 더 많은 로직을 추가하여 복잡한 상태 관리를 할 때 사용된다.
⑤ useCallback : 불필요한 렌더링을 방지하고자 할 때 사용된다.
⑥ useMemo : 계산 비용이 높은 값을 캐싱하거나, 불필요한 계산을 방지할 때 사용된다.
⑦ useRef : 함수 컴포넌트에서 DOM 요소에 접근할 때 사용된다.
함수 컴포넌트 내에서만 사용 : Hook은 함수 컴포넌트 내에서만 사용할 수 있다. 클래스 컴포넌트나 일반 함수 외부에서 Hook을 호출하면 동작하지 않는다.
최상위에서만 호출: Hook은 항상 함수 컴포넌트의 최상위에서만 호출한다. 조건문, 반복문, 중첩 함수 등의 내부에서 호출하면 예상치 못한 동작이 발생할 수 있다.
조건부 내에서 사용하지 말 것 : 조건문 내에서 Hook을 사용하지 않아야 한다. 항상 렌더링 과정에서 Hook이 동일한 순서로 호출되어야 하기 때문이다.
이름 규칙 : Hook은 "use"로 시작하는 이름으로 작성되어야 한다. 이는 리액트가 Hook을 식별하고 구분하기 위한 규칙이기 때문이다.
( ex> useState, useEffect 등 )
* 참고 :
https://react-ko.dev/reference/react/hooks
https://ko.legacy.reactjs.org/docs/hooks-intro.html#gatsby-focus-wrapper
https://surviveasdev.tistory.com/entry/React-hook이-나온-이유와-사용해야-하는-이유
https://velog.io/@remon/React-Hook에-대하여