전체내용 : Hook의 개요
1. 개요
Hook을 이용하면 Class를 작성할 필요 없이 상태 값(state)과 여러 React의 기능을 사용할 수 있다.
지원버전
React 16.8.0 이상
React Native 0.59 이상
1) 특징
- 선택적 사용
선택적으로 일부 컴포넌트에서 Hook사용 가능. - 100% 이전 버전과의 호환성
Hook은 호환성을 깨뜨리는 변화가 없다. - React 컨셉을 대체하지 않는다.
대신, React개념(props, state, context, refs, lifecycle, etc)에 좀 더 직관적인 API제공하고, 이 개념들을 엮기 위해 새로운 강력한 방법 제공
2) 동기
기존 React는 컴포넌트에 재사용 가능한 행동을 붙이는 방법을 제공하지 않았다. 이를 해결하기 위해 Render props, 고차 컴포넌트와 같은 패턴을 이용했다. 하지만 이런 패턴은 컴포넌트를 재구성해야 하며 코드를 추적하기 어렵게 만들었다. 따라서 React는 상태 관련 로직을 공유하기 위해 좀 더 좋은 기초 요소가 필요했다.
Hook을 사용하면,
컴포넌트로부터 상태 관련 로직을 추상화할 수 있다. 이는 독립적인 테스트와 재사용이 가능하다. Hook은 계층 변화 없이 상태 관련 로직을 재사용할 수 있도록 도와준다.
→ Hook을 공유하기 쉬워진다.
3) Hook 이전의 문제점
(1) 복잡한 컴포넌트들은 이해하기 어렵다
각 생명주기 메서드는 자주 관련 없는 로직이 섞여 있다. 예를들어, componentDidMount
메서드에서 이벤트 리스너를 설정하는 것과 관계없는 일부 로직이 포함되거나, componentWillUnmount
에서 cleanup을 수행하기도 한다. 이런 점으로 인해 버그가 쉽게 발생하고 무결성을 쉽게 해친다.
위와 같은 사례 안에서 상태 관련 로직이 모든 공간에 있기 때문에 이러한 컴포넌트들을 작게 만드는 것은 불가능하고 테스트하기 어렵다.
(← 많은 사람이 React를 별도의 상태 관리 라이브러리와 함께 결합해서 사용하는 이유 중 하나)
그러나, 종종 상태 관리 라이브러리는 너무 많은 추상화를 하고, 다른 파일들 사이에서 건너뛰기를 요구하며 컴포넌트 재사용을 더욱더 어렵게 만든다.
이를 해결하기 위해, 생명주기 메서드를 기반으로 쪼개는 데 초점을 맞추는 대신, Hook을 통해 로직에 기반을 둔 작은 함수로 컴포넌트를 나눌 수 있다.
(2) Class는 사람과 기계를 혼동시킨다
Class는 코드의 재사용성과 코드 구성을 더 어렵게 만든다. 또한, 잘 축소되지 않고, 핫 리로딩을 깨지기 쉽고 신뢰할 수 없게 만든다. 이에 React개발자들은 코드가 최적화 가능한 경로에서 유지될 가능성이 더 높은 API를 제공하고 싶었다.
따라서, Hook은 Class없이 React기능들을 사용하는 방법을 알려준다. Hook은 명령형 코드로 해결책을 찾을 수 있게 해준다. 개념적으로 React컴포넌트는 항상 함수에 더 가깝다. Hook은 React의 정신을 희생하지 않고 함수를 받아들인다.
2. Hook 이 뭔가요 ?
1) Hook 개념
Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 연동(hook into)할 수 있게 해주는 함수이다. class안에서는 동작하지 않는 대신, class없이 react를 사용할 수 있게 해준다.
React는 useState와 같은 내장 Hook을 몇 가지 제공한다. 컴포넌트 간에 상태 관련 로직을 재사용하기 위해 Hook을 직접 만드는 것도 가능하다.
2) Hook 사용 규칙
Hook은 JavaScript함수이지만, 두 가지 규칙을 준수해야 한다. 이 규칙들을 강제하기 위해 linter plugin
을 제공한다.
(1) 최상위(at the top level)에서만 Hook을 호출해야 한다.
반복문, 조건문, 중첩된 함수 내에서 Hook 실행 금지
(2) React 함수 컴포넌트 내에서만 Hook을 호출해야 한다.
일반 JavaScript함수에서는 Hook을 호출하면 안 된다. (단, 직접 작성한 custom Hook 내부에서는 Hook 호출 가능)
3. 기본 내장 Hook
1) State Hook (=useState)
(1) state 변수 선언
import React, { useState } from 'react';
function Example() {
// "count"라는 새 상태 변수 선언
const [count, setCount] = useState(0);
...
useState가 Hook이다!
Hook을 호출해 함수 컴포넌트 안에 state를 추가한 모습이다. 이 state는 컴포넌트가 다시 렌더링 되어도 그대로 유지된다. (class의 this.setState와 유사하지만, 이전 state와 새로운 state를 합치지 않는다는 차이가 있다.)
useState는 인자로 초기 state값을 하나 받는다. 초기값은 첫 번째 렌더링에만 딱 한 번 사용된다.
Hook의 state는 객체일 필요가 없다. (원한다면 객체로도 가능)
(2) 여러 state 변수 선언
function ExampleWithManyStates() {
// 상태 변수 여러개 선언!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
...
}
배열 구조 분해(destructuring) 문법은 useState로 호출된 state 변수들을 다른 변수명으로 할당할 수 있게 해준다. React는 매번 렌더링할 때 useState가 사용된 순서대로 실행한다. (이유는 나중에 ~)
2) Effect Hook (=userEffect)
side effects(또는 effects) : React 컴포넌트 안에서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업 (다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업)
useEffect는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 해준다. (React class의 componentDidMount
, componentDidUpdate
, componentWillUnmount
와 같은 목적으로 제공되지만 하나의 API로 통합된 것)
(1) effect 선언
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// componentDidMount, componentDidUpdate와 비슷
useEffect(() => {
// 브라우저 API를 이용해 문서의 타이틀을 업데이트함
document.title = 'You clicked ${count} times';
});
...
useEffect
를 사용하면, React는 DOM을 바꾼 뒤에 effect 함수를 실행한다.
Effects
는 컴포넌트 안에 선언되어있으므로 props와 state에 접근 가능하다. 기본적으로 React는 매 렌더링 이후에 effects를 실행한다. 첫 번째 렌더링도 포함해서
useState와 마찬가지로 컴포넌트 내에서 여러 개의 effect 사용 가능
Hook을 사용하면 구독을 추가하고 제거하는 로직과 같이 관련 있는 코드들을 모아서 작성할 수 있다.
(반면 class 컴포넌트에서는 생명주기 메서드에 각각 쪼개서 넣어야만 했다.)
function Example() {
useEffect(() => { <내용> });
useEffect(() => { <내용> });
...
(2) effect 해제 (Optional)
만약, Effect를 해제할 필요가 있다면, 해체하는 함수를 반환해주면 됨.
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
위 예시에서 컴포넌트가 unmount될 때 React는 ChatAPI에서 구독을 해지하거나, 재 렌더링이 일어나 effect를 재실행하기 전에 구독을 해지한다.
3) 다른 내장 Hook
보편적이진 않지만 유용한 내장 Hook Hooks API Reference참고
(1) useContext
컴포넌트를 중첩하지 않고도 React context를 구독할 수 있게 해준다.
function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
...
}
(2) useReducer
복잡한 컴포넌트들의 state를 reducer로 관리할 수 있게 해준다.
function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
...
}
4. 나만의 Hook 만들기
개발을 할 때 상태 관련 로직을 컴포넌트 간에 재사용하고 싶은 경우, Hook은 컴포넌트 트리에 새 컴포넌트를 추가하지 않고도 가능하다.
- Custom Hook은 컨벤션(convention)에 가깝다.
이름이 'use'로 시작하고, 안에서 다른 Hook을 호출한다면 그 함수를 custom Hook 이라고 부를 수 있다. - linter플러그인이 Hook을 인식하고 버그를 찾으려면
useSomething
이라는 네이밍 컨벤션 사용
custom Hook 사용 예시
//friendID를 인자로 받아서 친구의 접속 상태를 반환하는 custom Hook
function useFriendStatus(friendID) {
...
return isOnline;
}
// custom Hook을 사용하는 컴포넌트
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
각 컴포넌트의 state는 완전히 독립적이다. Hook은 state 그 자체가 아니라, 상태 관련 로직을 재사용하는 방법이다. 실제로 각각의 Hook 호출은 완전히 독립된 state를 가진다. 그래서 한 컴포넌트 안에서 같은 custom Hook을 두 번 쓸 수도 있다.