Hook은 함수 컴포넌트에서 상태 관리와 생명주기 기능(lifecycle features)을 연동(hook into)할 수 있게 해주는 함수입니다.
Hook은 React 버전 16.8부터 새로 추가된 기능입니다. Hook을 사용하려면 모든 React 패키지(ex. React DOM)가 16.8.0 이상이야 합니다. 패키지를 모두 업데이트하지 않으면 Hook이 작동하지 않습니다. Hook이 등장하고 가장 달라진 점은, 클래스 컴포넌트에서만 사용할 수 있던 유용한 기능들을 함수 컴포넌트에서도 사용할 수 있게 되었다는 것입니다.
함수 컴포넌트는 상태를 가질 수 없고, 메서드를 사용해 생명주기를 관리할 수 없습니다. 그래서 기존에는 코드의 복잡성과 낮은 재사용성 등의 여러 문제점에도 불구하고 클래스 컴포넌트를 사용해야만 했습니다. 하지만 Hook이 등장하면서 함수 컴포넌트를 이용해 더욱 간결하고 효율적인 코드로 상태 관리 및 생명주기 기능을 사용할 수 있게 되었습니다.
1.1.2 Hook의 종류
React는 다음과 같은 내장 Hook을 제공하고 있습니다.
그림 1-1 React에 내장된 Hook API
기본 Hook은 상태 관리와 생명주기 기능을 위해 가장 많이 사용됩니다.
추가 Hook은 기본 Hook을 변경하거나 특정한 경우에 필요에 따라 사용합니다.
Hook 라이브러리에서는 여러 유용한 기능을 제공하고 있어 Custom Hook을 만들기 전에 살펴보기 좋습니다.
이 외에도 Custom Hook이라고 불리는 자신만의 Hook을 만드는 것도 가능합니다. Custom Hook을 만들면 컴포넌트 간에 상태 관련 로직을 함수로 뽑아내어 재사용할 수 있습니다. 이는 캡슐화(capsulization)라고도 합니다.
Hook의 종류는 이처럼 다양하고, 이 책에서 하나씩 천천히 다뤄볼 예정입니다.
1.2 Hook의 특징
1.2.1 이전 버전과의 호환성
Hook의 사용은 이전 버전의 코드와 충돌을 발생시키지 않습니다. Hook은 React 이전 버전과 100% 호환이 되기 때문에 기존 코드의 무결성을 해치지 않고도 최적의 성능을 낼 수 있습니다.
1.2.2 선택적 사용
Hook은 기존의 코드 내에서 필요에 따라 선택적으로 사용할 수 있다는 장점이 있습니다. 기존의 코드를 수정하지 않고도 일부 컴포넌트 안에서 Hook을 사용할 수 있습니다. 이미 작성한 컴포넌트를 재작성할 필요 없이 새로 작성하는 컴포넌트부터 Hook을 사용할 수 있습니다. 이것은 아래의 ‘Class와 호환성’과도 이어집니다.
1.2.3 Class와 호환성
Hook이 기존 함수 컴포넌트에서 클래스 컴포넌트의 일부 기능을 ‘연동’할 수 있도록 도와주기 때문에 더 이상 Class는 React에서 사용할 수 없다는 불안함이 있을 수 있습니다. 하지만 앞으로도 React는 Class를 지원할 예정입니다. 다음은 React 공식 문서에서 발췌한 내용입니다.
Hook은 존재하는 코드와 함께 나란히 작동함으로써 점진적으로 적용할 수 있습니다. (중략)
React의 개발자들은 현재 사용 중인 Class 사례를 Hook으로 교체하는 것을 염두에 두고는 있지만, 미래에도 계속 클래스 컴포넌트들을 지원할 예정입니다.
또한, 클래스 컴포넌트 내부에서Hook을 사용할 수는 없지만, 클래스 컴포넌트와 함수 컴포넌트를 단일 트리에서 Hook과 섞어서 사용할 수 있습니다. 컴포넌트가 클래스 컴포넌트인지 Hook을 사용하는 함수 컴포넌트인지 여부는 해당 컴포넌트의 구현 세부 사항일 뿐입니다.
1.3 Hook의 사용 규칙
1.3.1 오직 React 함수 내에서 Hook을 호출해야 합니다.
Hook은 일반 JavaScript 함수나 클래스 컴포넌트에서는 호출할 수 없으며 React 함수 컴포넌트, Custom Hook에서 호출할 수 있습니다.
1.3.2 최상위(at the top level)에서만 Hook을 호출해야 하고 훅을 호출하는 순서는 항상 같아야 합니다.
컴포넌트 안에서는 같은 훅을 여러 번 호출할 수 있는데 이때 React는 Hook이 호출된 순서에 의존하여 상태 값을 구분하고 기억합니다. 조건에 따라 Hook이 호출되지 않거나 실행 횟수가 변한다면 Hook의 실행 순서가 달라지고 그로 인해 오류가 발생할 수 있습니다. 따라서 조건문, 반복문, 중첩된 함수 내에서는 Hook의 사용을 지양해야 합니다. 최상위에서 Hook이 호출되면 컴포넌트가 렌더링 될 때마다 Hook의 순서는 항상 동일하게 보장됩니다.
// 올바른 예제
useEffect(function Form() {
if (name !== '') {
console.log("😀");
}
});
// 올바르지 않은 예제 (조건문 내에 Hook이 위치함)
if (name !== '') {
useEffect(function Form() {
console.log("😀");
});
}
참고) 원활한 Hook의 사용을 위해 React는 위의 규칙을 강제해주는 eslint-plugin-react-hooks라는 ESLint 플러그인을 제공합니다. 이는 Create React App을 통해 React 프로젝트를 생성하면 기본적으로 포함되어 있으며 아래와 같은 명령어로 사용할 수 있습니다.
npm install eslint-plugin-react-hooks --save-dev
// ESLint 설정 파일
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
}
}
1.4 Hook을 사용하는 이유
앞서 설명했듯이 함수 컴포넌트에서는 상태관리와 생명주기 메서드를 이용할 수 없기 때문에 로직의 재사용 및 구조 파악이 어렵고 코드가 복잡한 클래스 컴포넌트를 사용할 수 밖에 없었습니다. 이해를 돕기위해 클래스 컴포넌트와 함수 컴포넌트를 간단히 비교하자면, 클래스 컴포넌트는 render()메서드를 반드시 포함해야 합니다. render() 메서드 내부에는 렌더링 하고 싶은 JSX 코드를 작성하면 됩니다. 또한 props를 조회하거나 상태를 업데이트하려면 this 키워드를 활용하여 this.props, this.setState를 사용해야 합니다. 따라서 this의 작동 방식에 대한 이해가 없다면 클래스 컴포넌트 활용에 어려움을 겪을 수 있습니다. 해당 부분은 1.4.3절에서 자세하게 살펴보겠습니다. 하지만 Hook이 등장한 이후 함수 컴포넌트에서 상태관리와 생명주기 이벤트 사용이 가능해져 위의 단점을 보완할 수 있습니다. 아래 클래스 컴포넌트와 함수 컴포넌트 예시 코드를 비교해보면 함수 컴포넌트가 더 간결하고 이해하기 쉽다는 것을 알 수 있습니다.
// 클래스 컴포넌트 예시
class Example extends React.Component {
constructor() {
super();
this.state = {title: "example"};
}
render() {
return <p>hello, {this.state.title}</p>
}
}
// 함수형 컴포넌트 예시
function Example () {
let [title, setTitle] = useState("example")
return <p>hello, {title}</p>
}
함수 컴포넌트의 사용을 더 편리하게 만든 Hook은 3가지 장점을 가지고 있습니다.
Hook을 통해 컴포넌트 간 상태 로직을 재사용 할 수 있습니다.
Hook은 구조를 이해하기 쉽습니다.
Hook은 클래스 컴포넌트 없이 React를 사용 가능하게 만듭니다.
React에서는 이와 같은 장점을 이유로 Hook을 사용하는 것을 권장하며 지금부터 공식문서에 정의된 내용을 바탕으로 Hook의 사용 동기를 이야기해보겠습니다.
1.4.1 Hook을 통해 컴포넌트 간 상태 로직을 재사용 할 수 있습니다.
프로그램을 만들다 보면 중복 코드를 쉽게 만날 수 있습니다. 이때 중복된 코드로 인해 발생하는 불필요한 리소스를 줄이고자 상황에 따라 코드를 재사용합니다. 마찬가지로 React에서도 중복 컴포넌트 로직들을 만날 수 있는데, React에서는 이러한 중복 컴포넌트를 재사용 할 수 있는 방법을 제공하지 않았습니다. 이를 해결하고자 클래스 컴포넌트에서는 Render props, 고차 컴포넌트(HOC, Higher Order Component)와 같은 재사용 패턴을 사용했습니다. 하지만 아래 예시 ① 부분처럼 재사용할 때마다 매번 컴포넌트를 감싸줘야 했기 때문에, 로직이 계속 겹쳐지면서 발생하는 래퍼 지옥(wrapper hell)을 만날 확률이 높아졌고, 코드를 추적하기 어려운 상황들이 발생했습니다.
// 고차 컴포넌트로 컴포넌트 재사용하기
// 고차 컴포넌트
function withLogin(WrapperComponent) {
return class extends React.Component {
// 생성자 및 공통 상태 로직
constructor() {
state = {
user: []
}
}
componentDidMount() {
// 비동기 작업 처리(데이터 받아오기)
}
// 내부 컴포넌트 렌더링
render() {
return <WrapperComponent
{...this.state}
{...this.props} />;
}
};
};
// 컴포넌트 재사용
function LoginPage ({ user }) {
// 내려받은 user 데이터 사용
}
export default withLogin(LoginPage); // --- ① 고차 컴포넌트로 감싸주기
이런 문제점들을 Hook을 통해 해결할 수 있게 되었습니다. Hook이 나온 이후 함수 컴포넌트에서 상태 관련 로직을 추상화할 수 있게 되면서, Custom Hook을 통해 컴포넌트 간 상태를 저장하고 공유할 수 있게 되었기 때문입니다. 이제는 고차 컴포넌트와 같은 패턴을 사용하지 않고도 Hook으로 손쉽게 컴포넌트를 재사용 할 수 있습니다.
컴포넌트의 구조가 복잡할 경우, 생명주기(Lifecycle) 또한 파악하기 힘들기 때문에 유지 보수가 어려워집니다. 또한 React는 컴포넌트 간 상태를 저장하고 공유할 수 없으므로 상태 관련 로직을 따로 관리하고자 컴포넌트로 작게 분리하는 것은 거의 불가능하며 테스트하기도 어렵습니다. 이를 별도의 상태 관리 라이브러리와 함께 결합하여 보완할 수 있으나, Hook을 사용하면 구조의 변화 없이 각각의 컴포넌트 내에서 상태를 관리하고 변경할 수 있기 때문에 React에 좀 더 직관적인 API를 제공할 수 있습니다.
💡
컴포넌트의 생명주기
모든 컴포넌트는 여러 종류의 ‘생명주기’를 가지며, 클래스 컴포넌트에서는 ‘생명주기 메서드’를 오버라이딩하여 특정 시점에 코드가 실행되도록 설정할 수 있습니다.
생명주기는 보통 페이지의 렌더링 준비 과정에서 시작하여 페이지를 업데이트하고 페이지에서 사라질 때 끝나게 됩니다. 이를 초기화(Initialize) - 생성(Mounting) - 업데이트(Props change, State Change) - 소멸(Unmounting)의 4단계로 표현할 수 있습니다.
컴포넌트가 생성되고 업데이트된 후 소멸되는 과정에서 생명주기 이벤트가 발생하게 되는데 해당 생명주기 단계에서 비동기적으로 사용자의 입력을 화면에 렌더링하거나 웹 서버와 통신하여 데이터를 조회하는 등의 업무를 처리할 수 있습니다.
→ Hook을 이용하면 함수 컴포넌트에서도 상태 관리(state)와 React의 기능(생명주기 이벤트 등)을 사용할 수 있습니다.
그림 1-2 React 컴포넌트의 생명주기
1.4.3 Hook은 클래스 컴포넌트 없이 React를 사용 가능하게 만듭니다.
클래스 컴포넌트는 함수 컴포넌트 보다 많은 기능을 가지고 있지만 코드가 길고 복잡합니다. 예를 들면 클래스 컴포넌트는 this 키워드의 작동방식을 제대로 이해하지 못해 예상치 못한 오류를 만납니다. this는 상황에 따라 조작 및 변경이 가능하기 때문에 오류를 수정할 때도 함수형 컴포넌트와 달리 길고 복잡한 코드를 작성해야 합니다. 또한 클래스 컴포넌트는 페이지 생성 속도가 느립니다. React 공식 문서에서 발췌해온 자료에는 Prepack 컴파일러를 활용해 클래스 컴포넌트를 테스트한 결과, 클래스 컴포넌트가 컴파일 속도에 부정적인 영향을 미쳤습니다.
이와 같은 클래스 컴포넌트의 문제를 해결하기 위해 함수 컴포넌트를 사용해야 합니다. Hook은 React에서 함수 컴포넌트를 보편적으로 사용할 수 있게 만드는 도구입니다. Hook이 나오기 전에는 state와 생명주기 기능을 사용할 수 없다는 이유로 함수 컴포넌트가 많이 쓰이지 못했지만 Hook의 등장 이후 함수 컴포넌트가 적극적으로 활용되었습니다. 아래 그림을 통해 Props Drilling과 state를 이용한 상태 관리를 비교해볼 수 있습니다. Props Drilling은 최하위 컴포넌트에서 props를 사용하기 위해 연결된 모든 상위 컴포넌트를 거쳐야 하는 반면 state를 이용하면 데이터가 필요한 컴포넌트에 바로 사용 가능합니다.
그림 1-3
또한 화살표 함수를 사용하여 this를 바인딩하지 않아 코드의 간결함을 유지할 수 있고 깊은 컴포넌트 트리 중첩이 일어나지 않기 때문에 속도가 빠릅니다. 마지막으로 개념적 의미에서 React 컴포넌트는 함수에 가깝습니다. 함수를 지향하는 React 정신을 잇기 위해서도 Hook의 사용을 권장합니다.