Hooks는 React에서 함수형 컴포넌트에서 상태관리를 보다 쉽게 하기위하여 생겨났다.
어떤게 다르게 어떤 종류가 있는지 살펴보고자 한다.
아래 모든 설명에 대한 실습을 해보기 위해서는 react설치가 필요하다.
npx create-react-app react_hooks
cd react_hooks
npm start
useState는 함수형 컴포넌트에서 상태를 관리하는 기본적인 Hook이다.
src디렉토리에 Counter.js를 만들어준다.
useState의 기본 사용법은 const [state, setState] = useState(initState);
으로
값인 state
와 값을 대입하는 setState
그리고 초기화값인 initState
로 구성되어있다.
Counter.js
import React, { useState } from 'react';
const Counter = () => {
const [value, setValue] = useState(0);
return (
<div>
<p>현재 값은 : {value}</p>
<button onClick={() => setValue(value + 1)}>숫자 더하기</button>
<button onClick={() => setValue(value - 1)}>숫자 빼기</button>
</div>
)
};
export default Counter;
그리고 이 Counter가 그려질 수 있도록 App.js를 수정하자
import React from 'react';
import './App.css';
import Counter from './components/Counter';
function App() {
return (
<div className="App">
<Counter />
</div>
);
}
export default App;
간단하게 숫자를 더하고 빼는 기능이 만들어졌다.
setState라는 함수를 통해 값을 전달하고, 전달받은 값으로 컴포넌트는 리렌더링 하게된다.
useState
를 사용해야만 한다. 여러개의 Hook을 사용해보자.Info.js를 만들어보자
import React, { useState } from 'react';
const Info = () => {
const [name, setName] = useState('');
const [nickName, setNickName] = useState('');
return (
<div>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<input type="text" value={nickName} onChange={(e) => setNickName(e.target.value)} />
<p>이름 : {name}</p>
<p>닉네임 : {nickName}</p>
</div>
)
};
export default Info;
이름과 닉네임을 입력해서 변경되는 간단한 예제이다.
App.js도 Info를 출력하도록 변경해준다.
import React from 'react';
import './App.css';
import Info from './components/Info';
function App() {
return (
<div className="App">
<Info />
</div>
);
}
export default App;
상태를 관리해야할 대상이 여러개라도 문제없이 가능하다.
useEffect는 컴포넌트가 렌더링 될 때마다 실행되는 함수이다. 렌더링 될 때마다 특정 기능이 실행되야 한다면 이 함수를 사용하면 된다.
이전에 만들었던 Info.js에 내용을 추가해보자.
useEffect(() => {
console.log('렌더링이 완료되었다.');
console.log(name, nickName);
});
그리고 웹에 들어가서 글자를 입력해보자.
글자를 입력할 때마다 콘솔을 열어보면
이렇게 출력되는것을 볼 수 있다.
useEffect는 상태가 변경될 때마다 실행되는데, 최초 마운트 되었을 때만 실행하고싶을 때는 두 번째 인자를 빈 배열로 넘겨준다.
useEffect(() => {
console.log('마운드 될 때만 실행된다.');
}, []);
글자를 입력하여도 다시 함수가 실행되지 않는다.
이전에는 어떤 값들이 변경될 때마다.. 혹은 최초 마운트시에만 실행되는것을 살펴보았다.
이번에는 특정 값이 변경될 때만 함수가 실행되도록 처리해보자.
useEffect(() => {
console.log(name);
}, [name]);
좀 전 빈값으로 넘겼던 배열 안에 name
변수를 넣어준다.
그러면 nickName 변경때는 반응을 하지 않지만, name이 변경되면 함수가 실행되는것을 볼 수 있다.
배열안의 변수는 props로부터 받아온 변수를 넣어도 되고, useState의 변수를 넣어도 된다.
예를들어 구독과 같은 기능을 개발했다면, 컴포넌트가 언마운트될 때 구독해지를 해야할것이다.
이와같은 경우라면 useEffect의 뒷정리 함수(clean) return
을 사용하면 됩니다.
Info.js의 useEffect를 수정한다.
useEffect(() => {
console.log('mount');
return () => {
console.log('unmount!!');
};
}, [name]);
그리고, App.js도 수정해준다.
import React, { useState, useEffect } from 'react';
import './App.css';
import Info from './components/Info';
function App() {
const [visible, setVisible] = useState(true);
const onSetVisible = () => {
setVisible(!visible);
};
return (
<div className="App">
<div>
{ visible ? '보이기' : '숨기기' }
<button onClick={onSetVisible}>상태 변경하기</button>
</div>
{ visible && <Info /> }
</div>
);
}
export default App;
App.js에서 버튼을 통해
결과를 확인해보면 버튼을 눌러 컴포넌트가 언마운트될 때 useEffect
의 return이 실행된다.
useReducer는 useState와 비슷하지만 더 다양한 상황에서 사용할 수 있는 Hook이다. 간단한 예제로 살펴보자.
위에서 만들었던 Counter.js에 조금 추가해본다.
import React, { useState, useReducer } from 'react';
const reducer = (state, action) => {
if (action.type === 'INCREMENT') {
return { value: state.value + 1 };
}
if (action.type === 'DECREMENT') {
return { value: state.value - 1 };
}
return state;
};
const Counter = () => {
...
const [state, dispatch] = useReducer(reducer, { value: 0 } );
return (
<div>
...
<p>(useReducer) 현재 값은 : {state.value}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>숫자 더하기</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>숫자 빼기</button>
</div>
)
};
export default Counter;
위와같이 useState와 비슷하지만 이번엔 useReducer로 숫자를 더하고, 빼는 기능을 만들었다.
결과는 useState와 동일하게 작동한다.
여기서 useState와 다른점을 살펴보면 실제 작동하는 reducer
함수가 컴포넌트의 외부에 있다는것을 볼 수 있다.
이전에 useState를 사용할 때에는 관리해야할 변수가 많아지면 그만큼 useState의 개수도 많아졌다.
useReducer를 사용하면 좀 더 간편하게 생성할 수 있다.
이전에 만들었던 Info.js와 유사한 페이지를 만들어보자.
import React, { useReducer } from 'react';
const reducer = (state, action) => {
return {
...state,
[action.name]: action.value,
}
};
const InfoReducer = () => {
const [state, dispatch] = useReducer(reducer, {
name: '',
nickName: '',
});
const onChange = e => {
dispatch(e.target);
};
return (
<div>
<input name="name" type="text" value={state.name} onChange={onChange} />
<input name="nickName" type="text" value={state.nickName} onChange={onChange} />
<p>이름 : {state.name}</p>
<p>닉네임 : {state.nickName}</p>
</div>
)
};
export default InfoReducer;
input의 name속성을 이용해서 e.target.name을 참조하여 useState해 준 것과 같은 방식으로 동작하는 함수이다.
useState를 사용하여 Info.js를 만들었을때와 동일하게 동작한다.
다만 useReducer를 사용하였을 때 input의 개수가 많아지더라도, 간결하고 깔끔한 코드를 유지할 수 있다.
useMemo는 함수형 컴포넌트에서 연산을 최적화할 수 있다. 리스트의 숫자들의 평균을 내주는 간단한 예제를 살펴보자.
import React, { useState } from 'react';
const Average = () => {
const getAverage = list => {
console.log('getAverage func!!');
if (!list || !list.length) {
return 0;
}
const sum = list.reduce((sum, item) => sum+item);
return sum / list.length;
};
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = e => {
setNumber(e.target.value);
};
const addItem = () => {
setList(list.concat(parseInt(number)));
setNumber('');
};
const listView = list.map(item => (<li key={item}>{item}</li>));
return (
<>
<input type="number" onChange={onChange} />
<button onClick={addItem}>추가</button>
<div>
평균 : {getAverage(list)}
</div>
<ul>
{listView}
</ul>
</>
);
};
export default Average;
평균값이 정상적으로 잘 작동한다.
이제 완성했다고 생각할지 모르겠으나, console을 살펴보자.
input에 숫자를 넣을 때마다 getAverage 함수가 호출되는것을 볼 수 있다. 숫자를 입력할 때에는 함수를 호출할 필요가 없을것이다.
이런경우 useMemo
를 사용하여 최적화 할 수 있다.
const avg = useMemo(() => getAverage(list), [list]);
위 useMemo를 만들고 출력을 avg로 해준다.
import React, { useState, useMemo } from 'react';
const Average = () => {
const getAverage = list => {
console.log('getAverage func!!');
if (!list || !list.length) {
return 0;
}
const sum = list.reduce((sum, item) => sum+item);
return sum / list.length;
};
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = e => {
setNumber(e.target.value);
};
const addItem = () => {
setList(list.concat(parseInt(number)));
setNumber('');
};
const listView = list.map(item => (<li key={item}>{item}</li>));
const avg = useMemo(() => getAverage(list), [list]);
return (
<>
<input type="number" value={number} onChange={onChange} />
<button onClick={addItem}>추가</button>
<div>
평균 : {avg}
</div>
<ul>
{listView}
</ul>
</>
);
};
export default Average;
이제 input에 입력할 때에는 getAverage가 호출되지 않는다.
useCallback은 useMemo와 비슷한 함수이다. 둘 다 렌더링 성능 최적화에 사용하는 함수로, useMemo는 숫자, 문자열, 객체처럼 일반 값을 재사용할 때 사용하고, 함수를 재사용하려면 useCallback을 사용하면 된다.
위 Average.js에서 onChange와 addItem 함수는 컴포넌트가 리렌더링될 때마다 새로 새성되고 있다. 성능 최적화를 위해 필요한 경우에만 다시 생성되도록 수정해보자.
import React, { useState, useMemo, useCallback } from 'react';
const Average = () => {
const getAverage = list => {
console.log('getAverage func!!');
if (!list || !list.length) {
return 0;
}
const sum = list.reduce((sum, item) => sum+item);
return sum / list.length;
};
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = useCallback(e => {
setNumber(e.target.value);
}, []); // 최초 렌더링시에만 함수를 생성한다.
const addItem = useCallback(() => {
setList(list.concat(parseInt(number)));
setNumber('');
}, [list, number]); // list 혹은 number가 변경이 있을때에 새로 생성한다.
const listView = list.map(item => (<li key={item}>{item}</li>));
const avg = useMemo(() => getAverage(list), [list]);
return (
<>
<input type="number" value={number} onChange={onChange} />
<button onClick={addItem}>추가</button>
<div>
평균 : {avg}
</div>
<ul>
{listView}
</ul>
</>
);
};
export default Average;
성능 최적화를 열심히 해보자.
useRef를 사용하여 등록버튼 누르면 포커스가 input으로 이동시켜보자.
import React, { useState, useMemo, useCallback, useRef } from 'react';
const Average = () => {
const inputEl = useRef(null);
const getAverage = list => {
console.log('getAverage func!!');
if (!list || !list.length) {
return 0;
}
const sum = list.reduce((sum, item) => sum+item);
return sum / list.length;
};
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = useCallback(e => {
setNumber(e.target.value);
}, []); // 최초 렌더링시에만 함수를 생성한다.
const addItem = useCallback(() => {
setList(list.concat(parseInt(number)));
setNumber('');
inputEl.current.focus();
}, [list, number]); // list 혹은 number가 변경이 있을때에 새로 생성한다.
const listView = list.map(item => (<li key={item}>{item}</li>));
const avg = useMemo(() => getAverage(list), [list]);
return (
<>
<input type="number" value={number} onChange={onChange} ref={inputEl} />
<button onClick={addItem}>추가</button>
<div>
평균 : {avg}
</div>
<ul>
{listView}
</ul>
</>
);`
};
export default Average;
input에 useRef변수를 ref로 연결해주면 된다.
.current
를 하면 실제 엘리먼트를 가리키고 .current.focus()
를 해주면 포커스 이동이 된다.