React Hooks 는 v16.8 에 도입된 개념으로서, 함수형 컴포넌트에서도 상태 관리를 할 수 있는 useState, 그리고 렌더링 직후 작업을 설정하는 useEffect 등의 기능을 제공합니다. 이에 대하여 한번 자세히 알아봅시다.
React Hook 이 2019년 2월 6일 v16.8 버전에 정식 탑재되었습니다!
지금은 이 기능을 사용하시려면 리액트 v16.8 이상 버전을 사용하셔야 합니다. 이번 튜토리얼에서는 CRA 를 통해서 리액트 프로젝트를 생성하겠습니다.
$ npx create-react-app react-hooks-sample
$ cd react-hooks-sample
혹은, 다음 샌드박스를 쓰셔도 됩니다:
State Hook 은 함수형 컴포넌트에서 변화 할 수 있는 상태를 사용 할 수 있게 해줍니다.
한번 useState
라는 Hook 함수를 사용해서 카운터를 함수형 컴포넌트로 구현해보겠습니다.
Counter.js 라는 파일을 만들어서 다음 코드를 작성해보세요.
import React, { useState } from 'react';
const Counter = () => {
const [value, setValue] = useState(0);
return (
<div>
<p>
<b>{value}</b>번 누르셨습니다.
</p>
<button onClick={() => setValue(value + 1)}>+1</button>
<button onClick={() => setValue(value - 1)}>-1</button>
</div>
);
};
export default Counter;
useState
함수의 파라미터로는 사용하고 싶은 상태의 기본값을 넣어줍니다. 우리는 현재 0을 기본값으로 사용하고 있습니다. useState 를 호출하면 배열을 반환하는데, 이 배열의 첫번째 원소는 현재 상태 값과, 두번째 원소는 이 값을 설정해주는 setter 함수입니다.
이제 App 에서 렌더링해볼까요?
import React, { Component } from 'react';
import Counter from './Counter';
class App extends Component {
render() {
return (
<div>
<Counter />
</div>
);
}
}
export default App;
이번에는 useState 를 사용해서 여러개의 상태를 관리해야 할 땐 어떻게 해야 하는지 볼까요?
Form.js 컴포넌트를 다음과 같이 만들어보세요:
import React, { useState } from 'react';
const Form = () => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const onSubmit = e => {
e.preventDefault();
alert(`${name} (${description})`);
setName('');
setDescription('');
};
return (
<form onSubmit={onSubmit}>
<input
placeholder="이름"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
placeholder="설명"
value={description}
onChange={e => setDescription(e.target.value)}
/>
<button type="submit">확인</button>
</form>
);
};
export default Form;
다 만드셨으면 App.js 에서 렌더링 하세요.
import React, { Component } from 'react';
import Form from './Form';
class App extends Component {
render() {
return (
<div>
<Form />
</div>
);
}
}
export default App;
여러 유용한 HOC 함수들이 들어있는 recompose 의 withState 를 사용해본 분이시라면, 사용방식이 매우 유사함을 느끼셨을 것 입니다. HOC 를 사용해서 구현하는 것과 비교했을때 주요 차이점은 완전히 함수형 컴포넌트 라는 것 입니다. HOC 를 사용할땐 결국 HOC 에서 클래스형 컴포넌트를 생성했죠. 하지만, Hooks 는 클래스형 컴포넌트로 한번 감싸는게 아니라, 온전한 함수형 컴포넌트에 기능을 부여해줍니다.
이번엔 useEffect
라는 함수에 대하여 알아봅시다. 이 함수는, 컴포넌트가 마운트 되거나 리렌더링이 마치고 나서 실행됩니다. componentDidMount
와 componentDidUpdate
와 비슷하다고 생각하시면 됩니다.
아까 전에 만들었떤 Counter 컴포넌트에서 useEffect
함수를 사용해보겠습니다:
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [value, setValue] = useState(0);
useEffect(() => {
// 이 함수는 render 가 마치고 난 다음에 실행됩니다!
console.log('rendered:', value);
});
console.log('rendering: ', value);
return (
<div>
<p>
<b>{value}</b>번 누르셨습니다.
</p>
<button onClick={() => setValue(value + 1)}>+1</button>
<button onClick={() => setValue(value - 1)}>-1</button>
</div>
);
};
export default Counter;
다시, Counter 를 App 에서 렌더링하세요:
import React, { Component } from 'react';
import Counter from './Counter';
class App extends Component {
render() {
return (
<div>
<Counter />
</div>
);
}
}
export default App;
잘 알아두세요. useEffect 에 넣은 함수는 컴포넌트가 render 를 마친 다음에 실행됩니다. 그러면, 이걸 사용해서 어떤 작업을 할 수 있을까요?
가끔씩은, 우리가 똑같은 작업을 componentDidMount 와 componentDidUpdate 에서 구현해야 할 때가 있습니다. 예를들어서 velog 에서 포스트 하단에 뜨는 다른 포스트를 누르면 같은 종류의 페이지에서 url 만 바뀌게 됩니다.
Before: /@velopert/eslint-and-prettier-in-react
After: /@velopert/react-component-styling
때문에 컴포넌트가 언마운트 -> 마운트를 거치는 것이 아니라, 주소를 가르키는 props 만 업데이트가 되는데요, 이에 따라 포스트를 새로 불러와야 하기에, 이런 로직이 구현되어있습니다:
componentDidMount() {
this.initialize();
const { hash } = this.props.location;
if (hash !== '') {
PostsActions.activateHeading(decodeURI(hash.split('#')[1]));
}
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.urlSlug !== this.props.urlSlug) {
PostsActions.unloadPost();
this.initialize();
}
}
컴포넌트가 새로 마운트 될 때에도 this.initialize
를 호출하고, 업데이트 될 때에도 urlSlug 부분이 바뀌면 this.initialize
를 호출하고있죠. useEffect
를 사용하면 이러한 중복 로직을 해결 해줄 수 있습니다. 물론, 함수형 컴포넌트여야 사용 할 수 있지만요.
useEffect
를 사용 하실 때 주의하실 점은 우리가 설정해준 함수가 render
가 될 때마다 실행된다는 점 입니다. 즉, props 나 state 가 바뀌지 않고 부모컴포넌트가 리렌더링 될 때에도 호출이 됩니다. 만약에 특정 상황에만 이 함수가 실행되게끔 하고 싶다면, useEffect
의 두번째 파라미터로 주시하고 싶은 값들을 배열 형태로 전달해주면 됩니다.
useEffect(() => {
// 이 함수는 render 가 마치고 난 다음에 실행됩니다!
console.log('rendered:', value);
}, [value]);
이렇게 하면, value 값이 바뀔 때만 useEffect 가 호출됩니다.
리액트 매뉴얼에서는 Hooks 사용에 있어서 두가지 준수해야 할 규칙을 규정하였습니다.
Hooks 는 반복문이나, 조건문이나, 감싸진 함수에선 사용하면 안됩니다.
Hooks 는 리액트 함수형 컴포넌트 내부에서만 사용하셔야 합니다. 일반 JavaScript 함수에서는 사용하면 안됩니다. 하지만, Custom Hook 에서는 괜찮습니다. (이에 대해선 아래 섹션에서 알아보게 됩니다.)
위 규칙을 준수하기 위해서 ESLint 플러그인 도 만들어졌습니다. 아마 이 플러그인은 Hooks 가 공식 릴리즈가 된다면 CRA 로 만든 프로젝트에 자동으로 적용 될 것으로 보입니다.
우리가 방금 배운 useState 와 useEffect 를 활용하면, 정말 다양한 작업들을 할 수 있습니다. 그리고 재사용 되는 로직들은 우리가 따로 Custom Hook 으로 만들어서 우리들만의 Hook 을 만들어서 사용 할 수있습니다.
한번 연습삼아 axios 로 웹 요청을 하는 Hook 을 만들어볼까요?
우선 axios 를 설치하세요.
$ yarn add axios
그리고 src 디렉토리에 hooks 디렉토리를 만들어서 우리들의 Custom Hook useRequest
를 작성해보겠습니다. 주석들을 꼼꼼히 읽어보면서 Hook 을 만들어보세요!
import { useEffect, useState } from 'react';
import axios from 'axios';
function useRequest(url) {
// loading, response, error 값을 다루는 hooks
const [loading, setLoading] = useState(false);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
// 렌더링 될 때, 그리고 url 이 바뀔때만 실행됨
useEffect(
async () => {
setError(null); // 에러 null 처리
try {
setLoading(true); // 로딩중
const res = await axios.get(url); // 실제 요청
setResponse(res); // response 설정
} catch (e) {
setError(e); // error 설정
}
setLoading(false); // 로딩 끝
},
[url] // url 이 바뀔때만 실행됨
);
return [response, loading, error]; // 현재 값들을 배열로 반환
}
export default useRequest;
이제 useRequest
를 사용하는 컴포넌트를 작성해볼까요? 연습용 API 는 JSONPlaceholder 에서 제공되는 API 들을 사용하겠습니다.
import React from 'react';
import useRequest from './hooks/useRequest';
const Post = () => {
const [response, loading, error] = useRequest(
'https://jsonplaceholder.typicode.com/posts/1'
);
if (loading) {
return <div>로딩중..</div>;
}
if (error) {
return <div>에러 발생!</div>;
}
/*
컴포넌트가 가장 처음 마운트 되는 시점은, Request 가 시작되지 않았으므로
loading 이 false 이면서 response 도 null 이기에
response null 체킹 필요
*/
if (!response) return null;
const { title, body } = response.data;
return (
<div>
<h1>{title}</h1>
<p>{body}</p>
</div>
);
};
export default Post;
와우~ HOC 나 Render Props 로 할 수 있는 것들을, Hook 으로도 할 수 있게 되었습니다. Hook 을 사용하면 정말 다양한 것들을 할 수 있습니다.
리액트 라이브러리에는 방금 봤던 useState, useEffect 말고도 다른 Hooks 들이 내장되어있습니다.
내장 Hooks 에 대한 상세한 정보는 여기 에서 확인 할 수있습니다.
그 중에서 정말 유용할 것 같은 Hook 몇가지만 소개하고 나머지는 추후에 정식 릴리즈가 되면 다시 정리하도록 하겠습니다.
const context = useContext(Context);
useContext
는 Context API 를 Hook 을 통해 사용 할 수 있게 해줍니다. Render Props 보다, HOC 를 사용하는 것 보다, 혹은 contextType 을 사용하는 것 보다 훨씬 편하게 느껴지는 군요!
Hooks 가 정식 릴리즈가 되면 아마 저는 앞으로 Context 를 사용 할 떄 Hook 으로 사용 할 것 같습니다.
useReducer
는 리덕스에서 리듀서를 사용하는 것과 유사한 방식으로 컴포넌트 상태 관리를 할 수 있게 해줍니다. 그렇다고 해서 이 Hook을 사용하기위해 사전에 리덕스를 설치해야 하는건 아닙니다. 그냥 컴포넌트내부에서 사용 할 수 있는 리듀서라고 생각하시면 됩니다.
useRef
는 함수형 컴포넌트에서도 ref 를 사용 할 수 있게 해주는 Hook 입니다.
React 의 Hooks 가 정식 릴리즈가 되면 다시 한번 리액트 생태계에 큰 바람이 불게 될 것 같습니다. 재사용 되는 로직을 따로 분리시키는 방법이 앞으로 구현 방식에 있어서 선택사항이 정말 많아졌네요. HOC, Render Props, 그리고 대망의 Hooks!!