클래스형 컴포넌트의 기능을 사용할 수 있는 함수형 컴포넌트라고 볼 수 있다.
life-cycle과 state관리 모두 가능하다.
재사용 가능한 로직의 관리가 쉽다는게 제일 큰 장점이다. 이는 훅이 단순한 함수이고 함수 안에서 다른 함수를 호출하는 것으로 새로운 훅을 만들 수 잇기 떄문이다. 따라서 리액트의 내장 훅과 다른 사람들이 만든 여러 커스텀 훅을 레고처럼 조립해서 쉽게 새로운 훅을 만들 수 있다.
훅을 사용하면 같은 로직을 한곳으로 모을 수 있어서 가독성이 좋다. 반면 클래스형 컴포넌트의 생명 주기 매서드는 서로 다른 로직이 하나의 메서드에 섞여 있어서 가독성이 좋지 않다.
또한 정적 타입 언어로 타입을 정의하기 쉽다는 장점도 있다.
useState
훅을 이용하면 함수형 컴포넌트에서도 상탯값을 관리할 수 있다.
import React, {useState} from 'react';
function Profile() {
const [name, setName] = useState(''); ❶
return (
<div>
<p> {`name is ${name}`}</p>
<input type='text' value={name} onChange={e=> setName(e.target.value)} /> ❷
</div>
);
}
❶
useState
훅은 배열에 두 값을 넣어서 반환한다.
배열의 첫 번째 원소는 상탯값 즉 state
인데 함수 호출 시 입력한 인수가 초깃값으로 사용된다. 두 번째 원소는 상탯값을 변경할 수 있는 함수이다.
배열의 비구조화 할당 문법을 이용해서 각 원소에 이름을 부여한다. 이처럼 useState
훅을 통해서 함수형 컴포넌트에서도 상태값을 사용할 수 있다.
❷
사용자가 키보드를 누를 때마다 setName 함수를 호출한다. onChange
속성값으로 입력되는 함수는 렌더링이 될 때마다 생성되므로 성능면에서 걱정될 수 있다. 이 문제의 해결을 위해 useCallback
훅을 제공한다.
useState
훅 하나로 여러 상태값을 하나의 객체에 담아서 관리할 수 있다.
import React, {useState} from 'react';
function Profile () {
const [state, setState] = useState({ name: "", age:0}); ❶
return (
<div>
<p> {`name is ${state.name}`}</p>
<p> {`age is ${state.age}`}</p>
<input
type='text'
value={state.name}
onChange={ e=> setState({ ...state, name:e.target.value})} ❷
/>
<input
type='number'
value={state.age}
onChange={ e=> setState({ ...state, age:e.target.value})} ❷
/>
</div>
)
}
❶
두 개의 상태값을 하나의 객체로 관리한다
❷
기존 클래스형 컴포넌트에서의 setState
메소드는 기존 상태값과 입력된 값을 병합하지만
useState
훅은 이전 상태값을 지운다.
따라서 ...state
와 같은 코드가 필요한 것이다.
더 나아가 하나의 객체로 상태값을 관리할 때 useReducer
훅이 제공된다.
useEffect
훅을 사용하여 함수형 컴포넌트에서도 life-cycle 함수를 이용할 수 있다.
클래스형 컴포넌트에서의 각각 생명주기 메소드에 대응하는 훅이 존재하는 것은 아니지만 useEffect
훅을 이요하면 비슷한 기능을 한곳으로 모을 수 있어서 가독성이 좋아진다.
API 호출하는 기능과 이벤트 처리 함수를 등록하고 해제하는 기능을 각각 훅으로 구현하고 두기능을 하나로 합쳤을 때 함수형 컴포넌트와 클래스형 컴포넌트의 모습들을 비교할 것이다.
그 전에 useEffect
훅의 사용법을 예제로 살펴보자
import React, ( useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount ] = useState(0);
useEffect( ()=> {
document.title = '업데이트 횟수 : ${count}`;
});
return <button onClick={ ()=> setCount(count +1) }> increase </button>
}
❶
useEffect
훅에 입력된 함수는 렌더링 결과가 실제 돔에 반영된 후 호출된다.
따라서 document.title
을 변경하는 코드를 클래스형 컴포넌트의 componentDidMount
와 componentDidUpdate
양쪽 메소드에 추가하면 같은 기능을 구현하게 될 것이다. 즉 class형 컴포넌트에서는 서로 다른 로직이 하나의 생명주기 메소드에서 작성됐지만, useEffect
훅을 사용하면 하나의 로직을 위한 코드를 한 곳으로 모을 수 있다.
❷
버튼을 클릭하면 다시 렌더링되고 렌더링이 끝나면 useEffect훅에 입력된 함수가 호출된다.
import React, ( useState, useEffect } from 'react';
function Profile({userID }) {
❶const [user, setUser] = useState(null);
❷ useEffect (
()=> {
getUserApi(userId).then(data => setUser(data) );
},
[userId],❸
);
return (
<div>
{!user && <p>사용자 정보를 가져오는 중...</p>
{ user $$ (
<>
<p>{`name is ${user.name}`} </p>
<p>{ `age is ${user.age}`} </p>
</>
)}
</div>
}
❶
API 결과값을 저장할 상탯값이다.
❷
useEffect
훅에서 API 통신을 하며, 받아온 데이터는 user
상태값에 저장한다.
❸
useEffect
훅에 입력된 함수는 렌더링할 때마다 호출되기 때문에 API통신을 불필요하게 많이 하게 된다. 이를 방지하기 위해 useEffect
훅의 두 번째 매개변수로 배열을 입력하면, 배열의 값이 변경되는 경우에만 함수가 호출된다. 여기서는 useId
값이 변경되는 경우에만 API통신을 하도록 설정한다.
import React from 'react';
class Profile extends React.Component {
state = {
user: null,
};
componentDidMount() { ❶
const { userId } = this.props;
getUserApi(userId).then(data => this.setState({ user:data}));
}
componentDidUpdate(prevProps) { ❶
const { userId } = this.props;
if (userId ~== prevProps.userId) { ❷
getUserApi(userId).then(data => this.setState({ user:data })
}
}
render() {
const {user } = this.state;
return //...
}
}
❶
첫 번째 렌더링 후에는 componentDidMount
메서드가 호출되고, 두 번째 렌더링부터는 componentDidUpdate
메서드가 호출되므로 API와 통신하는 코드를 양쪽에 작성해야한다.
❷
단, componentDidUpdate
메소드에서는 userId
가 변경된 경우에만 API를 호출한다. 이처럼 클래스형 컴포넌트에서는 중복된 코드가 여러 생명주기 메소드에 흩어져있는 경우가 많다.
import React, ( useState, useEffect } from 'react';
function WidthPrinter() {
const [width, setWidth] = userState(window.innerWidth);
useEffect( ()=> {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize); ❶
❷ return () => {
window.removeEventListener('resize', onResize);
};
}, []);❸
return <div>{`width is ${width}`}</div>
}
❶
창 크기가 변경될 떄마다 onResize
함수가 호출되도록 등록한다
❷
useEffect
훅의 첫 번째 매개변수에 등록된 함수가 또 다른 함수를 반환할 수 있다.
반환된 함수는 컴포넌트가 언마운트되거나 첫 번째 매개변수로 입력된 함수가 호출되기 직전에 호출된다.
따라서 첫 번째 매개변수로 입력된 함수가 반환한 함수는 프로그램이 비정상적으로 종료되지 않는다면 반드시 호출될 것이 보장된다.
❸
useEffect
훅의 두 번째 매개변수에 빈 배열을 넣으면 컴포넌트가 마운트될 때만 첫 번째 매개변수로 입력된 함수가 호출되고,
컴포넌트가 언마운트될때만 반환된 함수가 호출된다.
import React, ( useState, useEffect } from 'react';
function Profile({userID }) {
const [user, setUser] = useState(null);
useEffect (
()=> {
getUserApi(userId).then(data => setUser(data) );
},
[userId],
);
const [width, setWidth] = userState(window.innerWidth);
useEffect( ()=> {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, []);
return (
<div>
{!user && <p>사용자 정보를 가져오는 중...</p>
{ user $$ (
<>
<p>{`name is ${user.name}`} </p>
<p>{ `age is ${user.age}`} </p>
</>
)}
</div>
}
리액트의 렌더링 성능을 위해 제공되는 훅이다.
훅을 사용하게 되면 컴포넌트가 렌더링될 때마다 함수를 생성해서 자식 컴포넌트의 속성값으로 입력하는 경우가 많다.
속성값이 매번 변경되기 때문에 자식 컴포넌트에서 불필요한 렌더링이 발생한다는 문제점이 있다.
이 문제점은 useCallback
훅으로 해결할 수 있다.
import React, ( useState } from 'react';
import { saveToServer } from './api';
import UserEdit from './UserEdit'
function Profile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
return (
<div>
<p>{`name is ${user.name}`} </p>
<p>{ `age is ${user.age}`} </p>
<UserEdit
onSave={ ()=> saveToServer(name,age)}
setName={setName}
setAge={setAge}
/>
</div>
);
}
위 예는 useCallback
훅이 필요한 코드다. 그 이유는 Profile
컴포넌트가 렌더링될 때마다 UserEdit
컴포넌트의 onSave
속성값으로 새로운 함수가 입력된다. 따라서 uerEdit
컴포넌트에서 불필요한 렌더링이 발생하는데 onSave
속성값은 name
이나 age
값이 변경되지 않으면 항상 같아야 한다.
import React, ( useState } from 'react';
import { saveToServer } from './api';
import UserEdit from './UserEdit'
function Profile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const onSave = useCallback( ()=> saveToServer(name,age), [name,age]); ❶
return (
<div>
<p>{`name is ${user.name}`} </p>
<p>{ `age is ${user.age}`} </p>
<UserEdit
onSave={onSave}
setName={setName}
setAge={setAge}
/>
</div>
);
}
❶
이전에는 onSave
속성값으로 전달했던 것과 같은 함수를 useCallback
훅의 첫 번째 매개변수로 입력한다.
두 번째 매개변수로 전달한 배열의 값이 변경되지 않으면 이전에 생성한 함수가 재사용된다.
따라서 name
과 age
값이 변경되지 않으면, UserEdit
컴포넌트의 onSave
속성값으로 항상 같은 함수가 전달된다
리덕스처럼 관리한다는 것은 여러 상탯값을 하나의 객체로 관리하는 경우에 용이하게 사용가능하다는 뜻이다
import React, { useReducer } from 'react';
❶
| const INITIAL_STATE = { name: 'empty', age: 0 };
| function reducer(state, action) {
| swtich (action.type) {
| case 'setName':
| return { ...state, name: action.name };
| case 'setAge':
| return { ...state, age: action.age };
| default:
| return state;
| }
| }
function Profile() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE); ❷
return (
<div>
<p>{`name is ${user.name}`} </p>
<p>{ `age is ${user.age}`} </p>
<input
type="text"
value={state.name}
onChange={ e=>
dispatch( { type: 'setName', name: e,.currentTarget.value }) ❸
}
/>
<input
type="number"
value={state.age}
onChange={ e=>
dispatch( { type: 'setAge', age: e,.currentTarget.value })
}
/>
</div>
)
}
❶
리덕스의 리듀서와 같은 방식으로 작성한 리듀서 함수다.
❷
useReducer
훅의 매개변수로 앞에서 작성한 리듀서와 초기 상태값을 입력한다.
useReducer
훅은 상태값과 dispatch
함수를 차례대로 반환한다.
❸
너무 좋은 글 잘보고 갑니다!! 덕분에 많이 배우고 가요🥳