Hooks는 리액트 v16.8에 새로 도입된 기능으로, 기존의 함수형 컴포넌트에서 할 수 없었던 다양한 작업들을 할 수 있게 해준다.
가장 기본적인 훅이며 함수형 컴포넌트에서 가변적인 상태를 지닐 수 있게 해준다. 간단한 버튼 카운터 프로그램을 만들어 보자.
export default function Counter() {
const [val, setVal] = useState(0);
function onClick() {
setVal(prev => prev + 1);
}
return (
<div>
<h1>{val}</h1>
<button onClick={onClick}>+1</button>
</div>
)
}
export default function App() {
return (
<div>
<Counter />
</div>
);
}
useState를 사용하기 위해 먼저 import 해주고, useState에 기본값을 넣어준다. (위의 예제에서는 0) 이 함수가 호출되면 리스트를 반환하는데 이 중 첫 번째 원소는 상태 값, 두 번째 원소는 상태를 설정하는 세터 함수이다.
useState는 하나의 상태 값만 관리할 수 있으므로, 컴포넌트에서 관리해야 하는 상태 값이 여러 개라면 useState를 여러 번 사용해야 한다.
useEffect는 리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook이다.
useEffect(() => {
console.log("rendered");
});
위의 카운터 프로그램에 useEffect를 import 하고, 렌더링될 때마다 콘솔에 출력하게끔 한다.
최초 실행 시 렌더링될 때 출력되었고 이후 버튼을 클릭할 떄마다 리렌더링 되므로 출력된다.
useEffect의 두 번째 파라미터로 비어 있는 리스트를 전달하면 된다.
그러면 버튼을 클릭해도 메시지가 출력되지 않는다.
useEffect는 최초 렌더링 이후 두 번째 파라미터로 넘겨 준 리스트 안의 값이 바뀔 때 실행된다.
useEffect의 첫 번째 파라미터의 함수를 수행하는 '조건' 을 걸어주는 것이다. 만약 리스트 안의 값이 아닌 다른 값이 변했다면, useEffect의 감시 대상이 아니기 때문에 실행하지 않는다.
즉 빈 리스트를 넘겨주면, 곧 '아무 값도 감시하고 있지 않다' 는 뜻이며 최초 렌더링 시점 이후에는 실행되지 않게 된다.
그러므로 두 번째 파라미터로 어떤 값을 넣은 리스트를 넘겨줄 수가 있다.
export default function Counter() {
const [a, setA] = useState("");
const [b, setB] = useState("");
function onChangeA(e) {
setA(e.target.value);
}
function onChangeB(e) {
setB(e.target.value);
}
useEffect(() => {
console.log("useEffect");
}, [a]);
return (
<div>
<input value={a} type="text" onChange={onChangeA}/>
<input value={b} type="text" onChange={onChangeB}/>
</div>
)
}
두 개의 input을 만들고, useEffect가 a만 감시하게끔 하였다.
그 결과 a input에 값을 입력할 때마다 useEffect가 출력되고, b input에 값을 입력하면 아무 일도 생기지 않는다.
컴포넌트가 언마운트되거나 업데이트되기 직전에 어떠한 작업을 수행하고 싶다면 뒷정리(cleanup) 함수를 리턴해주면 된다.
export default function Counter() {
const [a, setA] = useState("");
function onChangeA(e) {
setA(e.target.value);
}
useEffect(() => {
console.log("useEffect");
return () => {
console.log("cleanup");
};
});
return (
<div>
<input value={a} type="text" onChange={onChangeA} />
</div>
);
}
위와 같이 리턴값에 cleanup을 출력하는 함수를 넣어 주고, App.js에서 Counter 컴포넌트를 마운트/언마운트를 토글할 수 있는 기능을 넣어 준다.
export default function App() {
const [visible, setVisible] = useState(false);
return (
<div>
<button
onClick={() => {
setVisible((prev) => !prev);
}}
>
{visible ? "숨기기" : "보이기"}
</button>
{visible && <Counter />}
</div>
);
}
버튼을 클릭할 때마다 Counter 함수가 마운트되고, 언마운트된다.
렌더링 될 때는 useEffect가, 버튼을 한번 더 클릭했을 때 (사라질 때)는 cleanup이 출력되는 모습을 볼 수 있다.
그리고 input에 값을 입력하면, cleanup 함수가 업데이트 되기 직전의 값을 보여주므로 계속해서 출력된다. 이런 상황이 싫고 오직 언마운트 될 때만 cleanup 함수가 출력되게끔 하고 싶다면 두 번째 인자로 빈 리스트를 전달하면 된다.
useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업테이트할 수 있는 훅이다. (reducer 개념은 뒤에 리덕스를 다룰 때 자세히 배울 예정)
리듀서는 현재 상태, 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태를 반환하는 함수이다.
리듀서 함수에서 새로운 상태를 만들 때에는 반드시 불변성을 지켜주어야 한다.
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { value: state.value + 1 };
case "DECREMENT":
return { value: state.value - 1 };
default:
return state;
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, {value: 0});
return (
<div>
<p>현재 카운터 값 <b>{state.value}</b></p>
<button onClick={() => dispatch({type: 'INCREMENT'})}>+1</button>
<button onClick={() => dispatch({type: 'DECREMENT'})}>-1</button>
</div>
);
}
useReducer의 첫 번째 파라미터에는 리듀서 함서를 넣고 두 번째 파라미터에는 해당 리듀서의 기본값을 넣어 준다.
이 Hook은 state 값과 dispatch 함수를 받아 오는데 여기서 state는 현재 가리키고 있는 상태, dispatch는 액션을 발생시키는 함수이다. useReducer의 가장 큰 장점은 컴포넌트 업데이트 로직을 컴포넌트의 바깥으로 빼낼 수 있다는 점이다.
이번에는 여러 상태 값을 관리해 보자.
function reducer(state, action) {
return {
...state,
[action.name]: action.value,
};
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, {
name: "",
nickname: "",
});
const {name, nickname} = state;
const onChange = (e) => {
dispatch(e.target);
};
return (
<div>
<h1>이름 : {name}</h1>
<h2>닉네임 : {nickname}</h2>
<input type="text" name="name" value={name} onChange={onChange} />
<input type="text" name="nickname" value={nickname} onChange={onChange} />
</div>
);
}
useState를 사용했다면 두 번을 사용해야 했었는데, useReducer을 사용하면 다소 복잡하긴 하나 한 번의 사용만으로도 여러 상태 값들을 컨트롤 할 수 있다.
함수형 컴포넌트 내에서 발생하는 연산을 최적화 할 수 있다.
일단 useMemo 없이, 리스트 안의 숫자들의 평균값을 계산하는 프로젝트를 만들었다.
const getAverage = numbers => {
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
}
export default function Average() {
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = e => {
setNumber(e.target.value);
}
const onInsert = () => {
setList([...list, parseInt(number)]);
setNumber('');
}
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((val, index) => (
<li key={index}>{val}</li>
))}
</ul>
<b>평균값 : {getAverage(list)}</b>
</div>
)
}
이 때, 버튼을 클릭할 때 뿐만 아니라 input 내용이 수정될 때에도 getAverage 함수가 호출된다. 이렇게 렌더링될 때마다 계산하는 것은 낭비이다.
이럴 때에 useMemo를 사용하면 좋다. 특정 값이 바뀌었을 때만 연산을 실행하고 원하는 값이 바뀌지 않았다면 이전 연산 결과를 그대로 사용한다.
export default function Average() {
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = e => {
setNumber(e.target.value);
}
const onInsert = () => {
setList([...list, parseInt(number)]);
setNumber('');
}
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((val, index) => (
<li key={index}>{val}</li>
))}
</ul>
<b>평균값 : {avg}</b>
</div>
)
}
useEffect와 비슷하게, 두번째 인자로 넣어준 리스트 안의 원소들이 바뀔 때만 앞의 함수를 호출한다.
useCallback은 useMemo와 상당히 비슷하다. 렌더링 성능을 최적화해야 하는 상황에서 사용한다.
컴포넌트의 렌더링이 자주 발생하거나, 렌더링해야 할 컴포넌트의 개수가 많아지면 최적화해주는 것이 좋다.
위의 Average.js 파일을 useCallback을 사용하여 최적화하면, 다음과 같다.
export default function Average() {
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = useCallback(e => {
setNumber(e.target.value);
}, []);
const onInsert = useCallback(e => {
const nextList = list
setList([...list, parseInt(number)]);
setNumber('');
}, [number, list]);
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((val, index) => (
<li key={index}>{val}</li>
))}
</ul>
<b>평균값 : {avg}</b>
</div>
)
}
useCallback의 첫 번째 파라미터에는 생성할 함수를 넣고, 두 번째 파라미터에는 배열을 넣는다. 이 배열에는 '값이 바뀔 때 함수를 새로 생성할 값' 이 들어간다.
onChange처럼 비어 있는 배열을 넘겨주면 렌더링될 때 처음만 함수가 생성되며 onInsert는 input 내용이 바뀌거나, list가 변경될 때마다 함수가 생성된다.
함수 내부에서 상태 값을 참조할 경우, 반드시 그 값을 배열에 포함시켜 주어야 한다. onInsert는 list와 number를 사용하므로, 두 값들을 배열 안에 넣어 주어야 한다.
함수형 컴포넌트에서 ref를 쉽게 사용할 수 있게 해준다.
Average 컴포넌트에서 버튼을 눌렀을 때 input으로 포커스가 넘어가게끔 해보자.
export default function Average() {
const [list, setList] = useState([]);
const [number, setNumber] = useState("");
const inputEl = useRef(null);
const onChange = useCallback((e) => {
setNumber(e.target.value);
}, []);
const onInsert = useCallback(
(e) => {
setList([...list, parseInt(number)]);
setNumber("");
inputEl.current.focus();
},
[number, list]
);
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} ref={inputEl} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((val, index) => (
<li key={index}>{val}</li>
))}
</ul>
<b>평균값 : {avg}</b>
</div>
);
}
useRef를 통해 만든 객체 안의 current 값이 실제 엘리먼트를 가리킨다.
여러 컴포넌트에서 비슷한 기능을 수행할 경우, 자신만의 Hook을 작성하여 로직을 재사용할 수 있다.
여러 개 input을 관리하기 위해 useReducer를 사용했던 로직을 useInputs
라는 Hook으로 따로 분리해 보자.
export default function Info() {
const [state, onChange] = useInputs({
name: "",
nickname: "",
});
const {name, nickname} = state;
return (
<div>
<div>
<input name="name" value={name} onChange={onChange} />
<input name="nickname" value={nickname} onChange={onChange} />
</div>
<div>
<div>
<b>이름 :</b>
{name}
</div>
<div>
<b>닉네임 : </b>
{nickname}
</div>
</div>
</div>
);
}
function reducer(state, action)
{
return {
...state,
[action.name]: action.value
};
}
export default function useInputs(initalForm) {
const [state, dispatch] = useReducer(reducer, initalForm);
const onChange = e => {
dispatch(e.target);
};
return [state, onChange];
}
[state, dispatch] = useReducer[reducer, (상태값)], reducer(state, action)
처럼 사용한다.useEffect, useMemo, useCallback 훅이 비슷해서 어떤 것을 사용해야 효율적일지 아직 잘 모르겠다...