Hooks는 리액트 v16.8에 새로 도입된 기능으로
함수형 컴포넌트에서도 상태 관리를 할 수 있는 useState
마운트, 업데이트, 언마운트 시에 작업을 처리할 수 있는 useEffect 등
다양한 작업을 할 수 있게 됐다.
클래스 컴포넌트는 여러 메서드들로 생명 주기 작업 처리를 해야되는데
복잡하고 관리하기 어려워질 수도 있다는 단점이 있다.
함수 컴포넌트에서 React Hooks를 활용하면
componentDidMount componentDidUpdate componentWillUnmount 등
클래스 컴포넌트에서는 나눠서 작성해야 됐던 작업들을
useEffect로 통합하여 관리하기가 좀 더 수월해졌다.
리액트에서 가장 기본적인 hook이며, 상태를 관리할 수 있게 해준다.
function App() {
const [count, setCount] = useState(0)
return (
<div>
지금 카운트는 <b>{count}</b>입니다.
</div>
)
useState 함수의 파라미터에는 상태의 기본값을 넣어주면 된다.
이 함수는 배열을 반환하는데
첫 번째 원소는 상태 값, 두 번째 원소는 상태를 설정하는 함수이다.
상태를 변화시키고 싶다면 setState(x)를 쓰면 된다.
setCount(1);
값이 변했으므로, 컴포넌트는 리렌더링이 된다.
useState는 하나의 상태 값만 관리할 수 있다.
여러 상태를 관리하고 싶다면 useState를 여러 번 쓰면 된다.
function App() {
const [count, setCount] = useState(0)
const [skill, setSkill] = useState('react');
return (
<div>
지금 카운트는 <b>{count}</b>입니다.
지금은 {skill} 사용 중...
<button onClick={() => setSkill('flutter')}>변경</button>
</div>
)
flushSync()는 React 18에서 추가된 함수인데
리액트에서는 setState가 비동기로 상태를 변경시키는데
flushSync() 안에서 setState를 호출할 경우 바로 DOM에 반영시킨다고 한다.
자주 사용할 경우 비동기 업데이트의 성능 최적화를 무시하고
동기적으로 처리하기 때문에, 성능에 영향을 미칠 수 있으므로
적절한 곳에 사용해야 한다.
//...
flushSync(() => {
setTodos(prev => {
const lastEl = prev[prev.length-1];
const newData: Item = {
id: lastEl.id +1,
task: input,
};
return [...prev, newData];
});
});
//...
useEffect는 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 hook이다.
componentDidMount와 componentDidUpdate를 합친 형태와 비슷하다.
보통 데이터를 가져오거나 이벤트를 등록할 때 많이 사용된다.
컴포넌트가 새로 렌더링될 때마다 useEffect를 실행시키고 싶다면 아래와 같이 작성하면 된다.
useEffect(() => {
console.log('마운트');
})
배열을 작성하지 않으면 이 useEffect는 렌더링될 때마다 작동하게 된다.
컴포넌트가 마운트될 때만 실행되게 하고 싶다면
의존성 배열을 빈 배열로 설정하면 된다.
// ...
useEffect(() => {
// 작업할 로직 작성
console.log('마운트');
// 의존성 배열
},[])
컴포넌트에서 특정 상태 값이 변할 때만 실행되게 하려면
배열에 상태 값을 넣어주면 된다.
// ...
useEffect(() => {
// 작업할 로직 작성
console.log('count가 변했습니다.');
// 의존성 배열
},[count])
count가 변할 때만 실행되게 된다.
return 안에 할 작업을 작성해 주면 된다.
이를 클린업(clean-up) 함수라고 한다.
등록된 이벤트를 없애는 작업에 사용되곤 한다.
// ...
useEffect(() => {
// 작업할 로직 작성
console.log('마운트');
return () => {
console.log('언마운트');
}
// 의존성 배열
},[])
useEffect와 사용 방법은 같지만 실행 시점이 다르다.
useLayoutEffect는 render 이후 DOM에 반영되기 전에
동기적으로 실행되는 함수이다.
useEffect는 render 이후 DOM에 반영된 후에
비동기적으로 실행되는 함수이다.
보통 useEffect만 써도 되지만,
성능의 문제나 애니메이션 시작 등, 화면에 바로 적용되어야 하는 요소가 있다면
useLayoutEffect를 쓰는 걸 고려해볼 수 있다.
useLayoutEffect는 동기적으로 작업되기 때문에
시간이 오래 걸리는 작업을 할 경우 작업이 끝난 후
DOM이 반영되므로 사용자 경험이 나빠질 수 있겠다.
useReducer는 useState와 같은 상태를 관리해주는 훅이다.
useState와 하는 역할은 비슷하지만
좀 더 복잡한 상태 관리 로직이 있을 경우 바깥으로 꺼내어 작성할 수 있다.
useReudcer는 Flux 패턴의 단방향 데이터 흐름을 비슷하게나마 구현할 수 있다.

Flux 패턴
리액트를 만든 meta(facebook)에서 제안한 패턴이다.
단방향 데이터 흐름이 핵심이다.
- Action: 상태 변경을 요청한다.
- Dispatcher: Action을 받아 Reducer(or Store)에게 전달한다.
- Store: 애플리케이션의 상태를 관리하며, Action에 따라 상태를 변경한다.
- View: 상태를 기반으로 UI를 렌더링한다.
<button onClick={() => dispatch({type: 'INCREMENT'})}>
+ 1
</button>
<button onClick={() => dispatch({type: 'DECREMENT'})}>
- 1
</button>
dispatch 함수로 상태 변경을 요청하는 함수이다.
const reducer = (state: number, action: Action) => {
switch(action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
// 개발자가 지정한 action type 외에는 오류를 발생시켜서
// state 기본 값을 보내는 것보단 잘못된 명령어임을 인지시키는 게 좋아보임.
// 보통 오타일 경우가 많을 것 같다.
throw new Error(`[ERROR] unknown action type - ${action.type}`);
}
};
dispatch에서 보내는 Action의 타입을 받아서 내부 로직을 처리 후 새로운 상태를 반환한다.
const [state, dispatch] = useReducer(reducer, 0);
reducer에서 관리하는 상태이다.
첫 번째 매개변수로 reducer 함수, 두 번째 매개변수로는 초기 상태를 전달한다.
<div>
<p>
현재 카운터 값은 <b>{state}</b>입니다.
</p>
<button onClick={() => dispatch({type: 'INCREMENT'})}>
+ 1
</button>
<button onClick={() => dispatch({type: 'DECREMENT'})}>
- 1
</button>
</div>
state 기반으로 UI를 렌더링하고, dispatch로 Action을 보낸다.
그리고 더 넓은 범위에서 상태를 공유할 수 있는 useContext 훅도 있다.
useMemo에서의 memo는 memozation을 뜻한다.
Memozation
동일한 계산을 반복해야할 경우, 한 번 계산한 결과를 메모리에 저장해둬서
중복 계산을 방지하는 방법이다.
리액트는 상태가 변할 때마다 컴포넌트를 다시 렌더링하게 되는데
만약 input의 상태를 관리하고 있다면 input의 값이 변할 때마다
onChange가 실행되고, onChange 함수는 input의 상태를 변경시키기 때문에
글자를 입력하거나, 지울 때마다 컴포넌트가 계속 리렌더링이 되어
효율이 떨어지게 된다.
이를 방지하기 위해서 상태의 값이 변경됐을 때만 리렌더링 할 수 있게
해주는 것이 useMemo이다.
const getAverage = (numbers: number[]) => {
console.log('계산 중...');
if (numbers.length === 0) return 0;
const sum = (a: number, b: number) => a+b;
return numbers.reduce(sum, 0);
}
만약 위의 함수가 매우 무거운 작업이라고 가정하고 아래의 컴포넌트를 만들었다고 가정하면
const Average = () => {
const [list, setList] = useState<number[]>([]);
const [number, setNumber] = useState('');
const onChnage = (e: React.ChangeEvent<HTMLInputElement>) => {
setNumber(e.target.value);
}
const onInsert = () => {
const inputNumber = parseInt(number);
if(isNaN(inputNumber)) return;
const nextList = list.concat(inputNumber);
setList(nextList);
setNumber('');
}
return (
<div>
<input type="text" value={number} onChange={onChnage}/>
<button onClick={onInsert} >등록</button>
<ul>
{
list.map((value, index) => (
<li key={index}>{value}</li>
))
}
</ul>
<div>
<p>평균 값: </p> {getAverage(list)}
</div>
</div>
)
};
이 컴포넌트는 input에 숫자가 입력될 때마다 렌더링이 되고
그때마다 getAervage 함수가 실행되게 된다.
getAverage가 매우 무거운 함수였다면, 렌더링되는 데 시간이 많이 소요되고
이는 사용자 경험이 나빠지는 것으로 이어지게 될 수도 있다.
그래서 list가 변할 때만 함수가 실행될 수 있도록 list 값을 기억해뒀다가
변경이 되었다면 getAverage 함수를 실행시켜서 반영시키는 것으로
최적화를 할 수 있다.
// ...
const avg = useMemo(() => {
return getAverage(list);
}, [list]);
return (
// ...
<p>평균 값: </p> {avg}
);
useCallback도 useMemo와 비슷하다.
useCallback은 함수 자체를 인자로 받아서 함수가 매 렌더링 때마다
다시 생성되는 걸 방지해주는 역할을 한다.
리렌더링될 때마다 함수 컴포넌트 안에 있는 내용들도
다시 생성되기 때문에 매번 새로운 함수 객체를 다시 할당받는 결과가 되고
이는 효율적이지 못하다.
물론 대부분의 상황에서는 문제가 없겠지만
컴포넌트가 자주 렌더링되는 상황이고 개수가 많아진다면 성능이 저하될 수도 있다.
// 컴포넌트가 처음 생성될 때만 함수를 생성
const onChangeUseCallback = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setNumber(e.target.value);
},[]);
// number 혹은 list가 변경되었을 때만 함수 생성
//
// []로 했을 경우 처음 생성됐을 상황을 기억하기 때문에
// number나, list는 0 그리고 []이 된다.
// 그래서 의존성 배열에 상태 값을 넣어줘야 한다.
const onInsertUseCallback = useCallback(() => {
const inputNumber = parseInt(number);
if(isNaN(inputNumber)) return;
const nextList = list.concat(inputNumber);
setList(nextList);
setNumber('');
}, [number, list]);
숫자나 문자열, 객체같이 일반 값을 재사용하는 경우에는 useMemo를
함수를 재사용할 경우에는 useCallback을 사용하면 좋다.
useRef는 렌더링에 필요하지 않은 값을 참조할 수 있게 해준다.
일반 변수는 리렌더링될 때마다 다시 할당이 되지만,
ref 객체는 동일한 객체를 반환하기 때문에 생명 주기 중 일정하게 정보를 기억할 수 있다.
useState로 관리되는 상태는 값이 변경될 경우 리렌더링이 되지만
ref 객체(ref.current)가 변경되더라도, 리렌더링이 되지 않기 때문에
정보를 기억할 수 있게 된다.
보통, DOM에 직접 접근해야될 때 DOM에 ref를 할당해서
포커스를 이동시키거나, 스크롤을 이동시키는 등의 작업을 할 수 있다.
const Average = () => {
const inputRef = useRef(null);
// ...
const handleClick = () => {
// ...
ref.current?.focus();
}
return (
<div>
<input ref={inputRef} value={value} onChange={onChange} /><br />
<button onClick={handleClick}>엔터!</button>
<p>입력한 값은 {value}입니다.</p>
</div>
);
}
비슷한 기능을 공유할 경우 따로 빼내어서 자신만의 훅으로 만들 수도 있다.
이름은 마음대로 지어도 되지만 보통 use를 앞에 붙여서 짓는다.
위에서 썼던 useReducer를 이용해서 커스텀 훅을 만든다고 한다면
import React, { useReducer } from "react";
const reducer = (state: any, action: React.ChangeEvent<HTMLInputElement>['target']) => {
return {
...state,
[action.name]: action.value,
};
};
const useInputs = (init: any): [any, (e: React.ChangeEvent<HTMLInputElement>) => void] => {
const [state, dispatch] = useReducer(reducer, init);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(e.target);
};
return [state, onChange];
}
export default useInputs;
이렇게 만들 수 있고, 이걸 실제 컴포넌트에서 쓸 경우에는
import useInputs from "./useInputs";
const 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>
);
}
이런 식으로 활용할 수 있다.
커스텀 훅을 써서 컴포넌트의 가독성이 많이 좋아진 걸 알 수 있다.
디바운스는 가장 마지막에 생성된 이벤트만 실행되는 것이라고 할 수 있다.
예를 들어, 검색을 한다고 했을 때
한 글자마다 검색 요청을 보내면 네트워크 요청을 많이 하게 되고,
서버나 클라이언트에 부담을 많이 주게 된다.
그래서 사용자가 입력을 마칠 때까지 기다린 후
입력이 끝났을 때 검색 요청을 보내서 효율적으로 검색을 할 때
디바운스를 활용할 수 있다.
// useDebounce.ts
import { useEffect, useState } from "react"
const useDebounce = (value: any, delay = 500) => {
const [state, setState] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setState(value);
}, delay);
return () => {
clearTimeout(handler);
}
},[value, delay]);
return state;
}
export default useDebounce;
import { useEffect, useState } from "react";
import useDebounce from "./useDebounce";
const Debounce = () => {
const [search, setSearch] = useState('');
const [data, setData] = useState([]);
const debounceValue = useDebounce(search);
useEffect(() => {
const getData = async() => {
return await fetch(`https://url.com/${debounceValue}`).then(res => {
if(!res.ok) {
return Promise.reject('No Data');
}
return res.json();
}).then(list => {
setData(list);
}).catch(err => console.error(err));
}
if(debounceValue) getData();
},[debounceValue]);
// ...
}