기존의 리액트는 클래스 컴포넌트를 기반으로 작업했다. 그러나 클래스 컴포넌트에는 몇몇 불편함이 존재했다. 클래스 컴포넌트에는 어떤 불편함이 있을까?
클래스 컴포넌트를 사용할 때는 this의 사용이 거의 필수적이다. 클래스 컴포넌트의 state나 props, 그리고 컴포넌트 내의 메소드를 사용하기 위해선 클래스 자신을 가리키는 this를 사용해야한다.
그러나 클래스 컴포넌트의 this는 한 가지 큰 문제점이 있었는데, 바로 this는 변경될 수 있다는 점이다. 자바스크립트의 this는 선언할 때의 가리킨 값이 고정되지 않고, 호출 되는 시점의 this로 변경된다. 변경될 수 있는 this는 대체 어떤 문제를 일으킨다는 것일까?
한가지 예시를 보자.

초라하긴 하지만.. 위 사진은 한 SNS 서비스의 프로필 페이지이다. 상단의 Selet Box를 통해 유저를 변경할 수 있고, 하단의 팔로우 버튼을 누르면 해당 유저를 팔로우 할 수 있다. 여기서 팔로우 버튼 컴포넌트의 코드를 보자.

팔로우 버튼 컴포넌트는 props로 user의 이름을 내려받고, 버튼을 클릭하면 3초 뒤에 해당 유저를 팔로우 한다. 예시에서는 setTimeout으로 지연시간을 발생 시켰지만, 실제 상황에서는 api 호출 등의 이유로 시간 지연이 발생할 수 있다.
여기서, 팔로우 버튼을 누르고 팔로우되는 과정 3초 사이에 유저가 변경이 된다면 어떻게 될까? 아래 움짤을 통해 결과를 확인할 수 있다.

분명 룩소 유저 페이지에서 룩소 유저를 팔로우 했지만 팔로우 과정 속에 우테코 유저로 변경하니 룩소 유저가 아닌 우테코 유저를 팔로우하는 문제가 발생했다.
이는 컴포넌트가 리렌더링되며 this.props.user가 가리키는 값이 룩소 유저가 아닌 우테코 유저로 변경 되었기 때문이다. 고정되지 않은 this로 인해 발생한 버그이다.
물론 this를 render() 함수 스코프 내에 변수로 저장 후 사용하는 방법과 같이 버그에 대한 해결책이 없는 것은 아니다. 하지만 이런 불변성이 없는 this들이 점점 많이 쌓이게 된다면 예상하지 못한 버그들을 계속해서 만나게 될 것이다.
클래스 컴포넌트에서 state를 활용한 로직을 재사용하기는 쉽지 않다. 이 전에는 HOC 패턴과 같은 방법으로 재사용성 문제를 해결했다. 그러나 HOC 패턴은 또 다른 문제를 낳았는데, HOC 패턴이 무엇인지, 그리고 이로 인해 발생하는 문제점은 무엇인지 아래 Hooks의 이점을 설명하면서 같이 설명하려고 한다.
뿐만 아니라 클래스 컴포넌트는 낮은 가독성, 함수형에 비해 비교적 낮은 성능 등 여러 단점들이 존재했다.
당시 많은 리액트 개발자들은 this의 문제로부터 자유로워지며 더 가독성 좋은 코드를 작성할 수 있는 함수형 컴포넌트를 사용하고 싶어했다.
사실, 당시에도 함수형 컴포넌트는 존재했다.

그러나 함수형 컴포넌트는 클래스형 컴포넌트에 비해 사용할 수 있는 기능이 매우 적었다.
클래스형 컴포넌트에서는 상태 관리와 생명 주기라는 핵심 기능을 사용할 수 있었던 것에 비해, 함수형 컴포넌트에서는 props로 값을 받고 화면을 렌더링해주는 기능이 전부였다. 물론, 클래스형 컴포넌트에서도 렌더링 기능이 포함되어있었다.
함수형 컴포넌트는 클래스형 컴포넌트에서 시키는 단순한 일만 하는 일종의 껍데기 컴포넌트 역할을 했었다.
그러나 Hooks의 등장 이후 React의 판도가 바뀌었다.

Hooks로 인해 함수형 컴포넌트에서도 드디어 상태 관리와 생명 주기 기능을 사용할 수 있게 되었다.
뿐만 아니라 앞서 말했던 this 문제로부터 해방되었고, 더 높은 가독성 등 함수형 만의 강점까지 가지고 있었다.
때문에 Hooks의 등장으로 인해 리액트 시장은 클래스형 컴포넌트에서 함수형 컴포넌트로 대세가 바뀌었다.
Hooks가 생기면서 함수형 컴포넌트에서 상태 관리, 생명 주기 관리 기능을 사용할 수 있다는건 알겠는데, 클래스형의 기능에 비해 어떤 이점이 있길래 리액트의 대세가 바뀐걸까?
Hooks의 몇가지 이점을 예시를 통해서 설명하겠다.
Hooks와 함수형 컴포넌트의 조합이라면 클래스형 컴포넌트보다 훨씬 깔끔하고 간단한 코드를 작성할 수 있다. 한가지 아주 간단한 예시를 보자.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
}
}
handleClick() {
this.setState({ count: this.state.count + 1});
}
render() {
return {
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
}
}
}
위 클래스형 컴포넌트는 초기값이 0인 count 상태를 선언하고, 버튼을 누르면 count + 1 되는 간단한 로직을 가지고 있는 컴포넌트이다. 이 코드를 함수형 컴포넌트로 변경한다면 어떻게 될까?
const Example = () => {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setCount(count + 1);
}
return {
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click me</button>
</div>
}
}
함수형 컴포넌트로 변환하면 위와 같은 코드가 된다. 딱 보면 느껴지다시피 클래스형 컴포넌트에 비해 매우 간단한 코드를 작성할 수 있다.
useState라는 Hook을 사용해서 상태와 상태 변경 함수를 명시적으로 선언할 수 있다.
또한, 클래스형 컴포넌트에서 필수적으로 사용해야 했던 this의 문제로 부터 해방된 모습도 볼 수 있다.
뿐만 아니라, Hooks를 사용하면 기존 생명 주기 함수를 사용하는 방식이 많이 변하게 된다.
그전에 리액트의 생명주기에 대해 아주 간단하게 설명을 하면,

리액트의 생명주기는 위 사진과 같다. 리액트는 크게 Mount, Update, Unmount 세 개의 생명주기를 가지고 있다.
Mount는 처음 컴포넌트가 생성되고 렌더링을 거치는 과정을 의미하며, 이 과정이 끝나면 클래스 컴포넌트에서는 componentDidMount() 함수를 실행한다.
Update는 props 혹은 state가 변경되었을 때, forceUpdate() 함수가 실행 되었을 때 리렌더링 하는 과정을 의미하고, 마찬가지로 이 과정이 끝나면 클래스 컴포넌트에서는 componentDidUpdate() 함수를 실행한다.
마지막으로 Unmount는 컴포넌트가 종료되는 과정을 의미하고 클래스 컴포넌트에서는 componentWillUnmount() 함수를 실행한다.
이 외에도 클래스형 컴포넌트는 여러 가지 생명주기 관련 함수들을 가지고 있지만, 위 세가지 함수들이 대표적으로 사용되는 생명주기 함수이다.

Hooks가 등장한 이후 부터는 앞서 말한 클래스형 컴포넌트의 대표적인 생명 주기 함수 세가지를 useEffect() 함수 하나로 모두 커버할 수 있게되었다.
예시를 보자.
// 클래스형 컴포넌트
componentDidMount() {
this.updateList(this.props.id);
}
componentDidUpdate() {
if(prevProps.id === this.props.id) return;
this.updateList(this.props.id);
}
위의 코드는 컴포넌트가 Mount 될 때 updateList() 함수를 실행하고, 또 id 프롭스가 업데이트 될 때 updateList() 함수를 실행하는 클래스형 컴포넌트의 코드이다.
위 코드를 useEffect() Hook을 사용한 코드로 바꾼다면 어떻게 될까?
// Hooks를 사용한 함수형 컴포넌트
useEffect(() => {
updateList(id);
}, [id]);
useEffect() 훅 하나로 두가지 라이프 사이클 함수의 기능을 아주 간단하게 작성했다.
한 가지 예시를 더 보자.
// 클래스형 컴포넌트
componentDidMount() {
document.body.style.overflow = 'hidden';
}
conponentWillUnmount() {
document.body.style.removeProperty('overflow');
}
위 코드는 컴포넌트가 Mount 될 때 스크롤 방지 스타일을 추가하고, Unmount 될 때 스크롤 방지 스타일을 다시 없애주는 코드이다.
아마 모달 컴포넌트 같은 곳에서 많이 사용되는 코드일 것이다.
이 코드에서 useEffect()를 사용한다면,
// Hooks를 사용한 함수형 컴포넌트
useEffect(()=> {
document.body.style.overflow = "hidden";
return () => document.body.style.removeProperty("overflow");
}, []);
useEffect() 함수 하나 내에서 Mount, Unmount 두가지 생명 주기에 대한 로직을 작성할 수 있게되었다.
이처럼 useEffect() Hook을 이용하면 쉽게 생명 주기 로직을 다룰 수 있다.
기존의 클래스형 컴포넌트는 상태 관리, 생명 주기 관리 측면에서 재사용성이 많이 낮았다.
이런 재사용성을 해결하기 위해 HOC패턴 등을 도입했다. 하지만 HOC 패턴에는 한 가지 치명적인 문제가 있었는데, 여기서 HOC란 무엇이고, 이로 인해 발생하는 문제점은 무엇일까?
HOC 패턴의 의미는 위와 같지만, 솔직히 글만 봐서는 HOC가 뭔지 이해하기가 힘들어 보인다. 한가지 예시를 보자.

두 가지 컴포넌트의 코드이다. 왼쪽은 사용자들의 나이 정보를 보여주는 페이지이고, 오른쪽은 사용자들의 직업 정보를 보여주는 페이지이다.
두 컴포넌트는 렌더링 하는 로직만 조금 다르고, 유저 정보를 fetch 해와서 상태로 저장하는 로직은 아예 동일하다. 중복된 유저 fetching 로직을 따로 분리해서 재사용성을 높이고 싶은데, 어떻게 하면 될까?

바로 usersHOC라는 고차컴포넌트를 만들어서 재사용성을 높일 수 있다. 두 페이지 컴포넌트에서 가지고 있던 유저 fetching 로직을 usersHOC 컴포넌트로 분리시켰다.

usersHOC 컴포넌트는 유저 fetching 로직을 사용하는 컴포넌트를 인자로 받고, 인자로 받은 컴포넌트에 users state를 넘겨준다.

각 페이지 컴포넌트에는 usersHOC 컴포넌트를 감싸면 유제 페칭 로직을 사용할 수 있다.
위와 같이 HOC 패턴은 클래스 컴포넌트에서 재사용성을 위해 추천되는 패턴이었다. 그러나 HOC 패턴에는 Wrapper 지옥이라는 커다란 문제가 있었다. Wrapper 지옥이란 무엇일까?
<MyComponent />
여기 임의의 컴포넌트 MyComponent가 있다. 만약 이 컴포넌트에 여러 로직들을 적용하고 싶다면 어떻게 사용하면 될까?
<ThemeHOC>
<UserHOC>
<AuthHOC>
<MyComponent />
</AuthHOC>
</UserHOC>
</ThemeHOC>
(Wrapping을 더 직관적으로 표현하기 위해 태그 형식으로 나타냄)
MyComponent에 테마 로직, 유저 관련 로직, 인증 로직을 적용하기 위해 각 고차컴포넌트들을 감싸줬다. 이렇게 고차 컴포넌트를 계속해서 감싸면 컴포넌트의 depth가 엄청 깊어지는 문제가 발생하는데, 이를 Wrapper 지옥이라고 한다.

조금은 극단적인 예시이긴 하지만, 이렇게 깊은 depth가 발생할 수도 있다. 이런 Wrapper 지옥 현상은 컴포넌트 구조를 알아보기 힘들게 하고 전체적인 복잡도를 크게 증가시킨다.
Hooks가 발표 되면서, Custom Hook이라는 개념도 같이 소개되었다.
Custom Hook이란,
useState, useEffect와 같이 리액트에서 제공하는 Hook들을 이용해 개발자가 직접 만드는 Hook을 의미하며, 상태 관리나 생명 주기 로직들을 추상화하여 재사용 가능하도록 만드는 함수를 뜻한다.
위에서 봤던 유저 fetching 로직을 커스텀 훅으로 만들면 다음과 같다.
const useFetchUser = () => {
const [users, setUsers] = useState([]);
useEffect(()=> {
fetchUsers().then((users) => {
setUsers(users);
});
}, []);
return { users };
}
일단 코드가 그냥 보기에도 usersHOC 컴포넌트보다는 훨씬 간단하고 읽기 쉽다.
users라는 상태를 선언하고, useEffect를 사용해 처음 마운트 될 때 fetchUsers 로직을 수행한다.
이렇게 만든 커스텀 훅은 사용처에서도 사용하기가 무척 쉬운데,
const UserAgePage = () => {
const { users } = useFetchUser();
return(
...
이처럼 useFetchUser 함수를 호출하는 단 한 줄만 추가해주면 쉽게 사용이 가능하다.
당연하게도, Wrapper 지옥같이 골치 아픈 문제는 발생하지 않는다.
이처럼 Custom Hook을 사용한다면 클래스형 컴포넌트의 재사용성 문제를 깔끔하게 해결할 수 있다.
끝으로
이 글에서는 Hooks의 탄생 배경을 알아보기 위해 클래스형의 단점과 Hooks의 장점만 주절주절 설명했지만, Hooks를 사용한 함수형 컴포넌트가 클래스형 컴포넌트에 비해 무조건적으로 좋은 것은 아니다.
호출 순서에 의존된다는 점이나, useEffect()로 모든 라이프 사이클 함수에 대응하지 못한다는 점 등 Hooks도 분명한 단점이 있다.
작성하는 컴포넌트의 역할에 맞게 적절한 컴포넌트 방식을 잘 선택하도록 하자.