● useState
useState는 여러개 쓰일 수 있다. 변경 될때마다 렌더링 된다!
● useEffect
리액트 컴포넌트가 렌더링 될 때마다 실행되는 Hook입니다. componentDidMount, componentDidUpdate 를 합친 형태라고 보아도 무방합니다.
App.js
import Info from './Info'
const App = ()=>{
return <Info />
}
export default App;
Info.js
import { useState, useEffect } from 'react';
const Info = ()=>{
const [name, setName] = useState('');
const [nickname, setNickname] = useState('');
useEffect(()=>{
console.log('렌더링이 완료되었습니다.');
console.log({name, nickname});
});
const onChange = (e)=>{
setName(e.target.value);
}
const onChangeNickname = e =>{
setNickname(e.target.value);
}
return(
<div>
<div>
<input value={name} onChange={onChange}/>
<input value={nickname} onChange={onChangeNickname}/>
</div>
<div>
<div>
<b>이름 :</b> {name}
</div>
<div>
<b>닉네임 :</b> {nickname}
</div>
</div>
</div>
)
}
export default Info;
위의 예제를 보면, state가 변경될 때마다 컴포넌트가 리렌더링되니까, useEffect가 계속 실행되는 걸 볼 수 있다.
⚬ 마운트될 때만 실행하고 싶을 때
만약에 마운트 될 때만 실행하고 싶다면 함수의 두 번째 파라미터에 빈 배열을 넣어주면 된다.
useEffect(()=>{
console.log('렌더링이 완료되었습니다.');
console.log({name, nickname});
}, []);
그럼 컴포넌트가 처음 나타날 때만 콘솔에 문구가 나타나고, 그 이후에는 나타나지 않는다.
⚬ 특정 값이 업데이트될 때만 실행하고 싶을 때
만약에, 클래스형 컴포넌트를 쓴다면 아래와 같이 수행할 것입니다. (말투 완전 번역재질)
componentDidUpdate(prevProps, prevState){
if(prevProps.value !== this.props.value){
doSomething(~~~)
}
}
그러면 이전의 props 값과 this의 props값을 비교해 달라진다면 특정 작업을 수행할 것입니다.
그렇다면 useEffect를 사용해서는 어떻게 할까욥?
useEffect(()=>{
console.log({name});
}, [name]);
두번째 파라미터로 전달되는 배열 안에 검사하고 싶은 값을 넣어주면 됩니다.
대부분의 경우 useEffect를 사용할 때 배열에 의존하는 값을 넣어줍니다. 빈배열이나 의존 값이 들어있는 배열을 넣어주는 경우는 있어도 배열을 아예 생략하는 상황은 거의 없다고 생각하면 됩니다.
⚬ 뒷정리
컴포넌트가 언마운트 되기 전이나 업데이트 되기 전에 어떠한 작업을 수행하고 싶다면 useEffect에서 뒷정리 함수를 반환해주어야합니다.
App.js
import { useState } from 'react'
import Info from './Info'
const App = ()=>{
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={()=>{
setVisible(!visible);
}}>{visible ? '숨기기' : '보이기' }</button>
<hr/>
{visible && <Info />}
</div>
)
}
export default App;
Info.js
useEffect(()=>{
console.log('effect');
console.log({name});
return ()=>{
console.log('cleanup');
console.log({name});
}
}, [name]);
처음에 뜨는 모습은 위와 같다.
보이기 버튼을 누르면 Info 컴포넌트가 보이게 되고,
콘솔창에는 위와 같은 모습이 뜨게 된다.
useEffect는 컴포넌트가 렌더링 될 때마다 실행되니까 컴포넌트 보이니까 실행됨.
다음 이름 김을 입력하면
useEffect가 실행되기전에 cleanup 함수가 실행된다.
name state의 이전 값이 실행되고 ( { name : '' } )
effect 가 실행되는 것을 볼 수 있다. {( name : 'ㄱ' )}
따라서 컴포넌트가 업데이트가 되기 이전에 cleanup함수가 실행되는 것이다.
만약에 컴포넌트가 사라질때만(언마운트) cleanup함수를 실행하고 싶다면, 두번째 파라미터에 빈배열을 적으면 된다.
useEffect(()=>{
console.log('effect');
console.log({name});
return ()=>{
console.log('cleanup');
console.log({name});
}
}, []);
그러면 보이기 버튼을 누를 때는 useEffect가 실행되고, 언마운트 될때 cleanup함수가 실행된다.
따라서 componentWillUnmount 메서드와 비슷한 효과를 낼 수 있다.
● useReducer
useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트 해주고 싶을 때 사용하는 Hook입니다. 리듀서는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션(action)값을 전달받아 새로운 상태를 반환하는 함수입니다.
리듀서 함수에서 새로운 상태를 만들 때는 반드시 불변성을 지켜야합니다.
function reducer(state, action){
return {...} //불변성을 지키면서 업데이트한 새로운 상태를 반환합니다.
}
액션 값은 주로 다음과 같은 형태로 이루어져 있습니다.
{
type: 'INCREMENT'
//다른값이 필요하다면 추가로 들어간다.
}
리덕스에서는 액션 객체에는 어떤 액션인지 알려주는 type 필드가 꼭 있어야하지만, useReducer에서 사용하는 액션 객체에는 반드시 type을 지닐 필요가 없습니다. 심지어 객체가 아니라 문자열이나 숫자여도 상관이 없습니다.
⚬ 카운터 구현하기
useReducer의 첫번째 파라미터에는 리듀서 함수를 넣고 두번째 파라미터에는 리듀서의 기본 값을 넣어줍니다.
이 Hook을 사용하면 state값과 dispatch 함수를 받아 오는데요, state는 가르키고있는 상태이고, dispatch는 액션을 발생시키는 함수입니다. dispatch(action) 과 같은 형태로, 함수 안에 파라미터로 액션값을 넣어주면 리듀서 함수가 호출되는 구조입니다.
useReducer를 사용했을 때의 가장 큰 장점은 컴포넌트 업데이트 로직을 바깥으로 빼낼 수 있다는 것입니다.
App.js
import Counter from './Counter';
const App = ()=>{
return <Counter/>
}
export default App;
Counter.js
import {useReducer } from 'react'
function reducer(state, action){
//action의 타입에 따라 다른 작업 수행
switch(action.type){
case 'INCREMENT' :
return {value : state.value + 1};
case 'DECREMENT' :
return {value : state.value -1 };
default :
return state;
}
}
const 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>
)
}
export default Counter;
⚬ 인풋 상태 관리하기
App.js
import Counter from './Counter';
import Info from './Info'
const App = ()=>{
return <Info />
}
export default App;
Info.js
import { useReducer } from 'react';
function reducer(state, action){
return {
...state,
[action.name] : action.value
}
}
const Info = ()=>{
const [state, dispatch] = useReducer(reducer, {name : '', nickname : ''})
const {name, nickname} = state;
const onChange = (e)=>{
dispatch(e.target)
}
return(
<div>
<input name="name" value={name} onChange={onChange}/>
<input name="nickname" value={nickname} onChange={onChange}/>
<hr/>
<p>이름 : {name}</p>
<p>닉네임 : {nickname}</p>
</div>
)
}
export default Info;
useReducer에서의 액션은 그 어떤 값도 사용이 가능합니다. 그래서 이번에는 이벤트 객체가 지니고 있는 e.target 값 자체를 액션 값으로 사용했습니다. 이런식으로 인풋을 관리하면 아무리 인풋의 개수가 많아져도 짧고 깔끔하게 유지할 수 있습니다.
● useMemo
useMemo를 사용하면 함수 컴포넌트에서 발생하는 연산을 최적화 할 수 있습니다.
먼저 useState를 활용해서 숫자들의 평균을 보여주는 함수 컴포넌트를 만들어보겠습니둥.
App.js
import Average from './Average';
const App = ()=>{
return <Average />
}
export default App;
Average.js
import { useState } from 'react';
const getAverage = (numbers)=>{
console.log("평균값 계산중...")
if(numbers.length === 0) return 0;
const sum = numbers.reduce((acc, cur)=> acc+ cur);
return sum / numbers.length;
}
const Average = ()=>{
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = (e)=>{
setNumber(e.target.value)
}
const onInsert = (e)=>{
const nextList = [...list, parseInt(number)];
setList(nextList);
setNumber('');
}
return(
<div>
<input value={number} onChange={onChange}/>
<button onClick={onInsert}>등록</button>
<ul>
{
list.map((value, idx)=> <li key={idx}>{value}</li>)
}
</ul>
<div>
<b>평균값 :</b> {getAverage(list)}
</div>
</div>
)
}
export default Average;
평균 값이 아주 잘 구해집니댜.
그런데!!
콘솔창을 보면, Input에 값을 입력할 때마다 우리가 만든 getAverage 함수가 호출되는 것을 볼 수 있슴니다.
getAverage() 함수가 input 값이 변경될 때마다 호출되는 이유는 React 컴포넌트에서 state 값이 변경될 때마다 해당 컴포넌트가 다시 렌더링되기 때문입니다. 따라서 렌더링할때마다 계산하는 것은 낭비입니다.
따라서 아래와 같이 수정하면 됩니댜.
Average.js
import { useState, useMemo } from 'react';
const getAverage = (numbers)=>{
console.log("평균값 계산중...")
if(numbers.length === 0) return 0;
const sum = numbers.reduce((acc, cur)=> acc+ cur);
return sum / numbers.length;
}
const Average = ()=>{
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const onChange = (e)=>{
setNumber(e.target.value)
}
const onInsert = (e)=>{
const nextList = [...list, parseInt(number)];
setList(nextList);
setNumber('');
}
const avg = useMemo(()=> getAverage(list), [list]);
return(
<div>
<input value={number} onChange={onChange}/>
<button onClick={onInsert}>등록</button>
<ul>
{
list.map((value, idx)=> <li key={idx}>{value}</li>)
}
</ul>
<div>
<b>평균값 :</b> {avg}
</div>
</div>
)
}
export default Average;
따라서 결론적으로 함수를 컴포넌트 바깥에 정의를 내려도, 컴포넌트가 리렌더링되면 (위의 예시는 state가 변경) 함수도 다시 호출되기때문에, 이럴 때 useMemo를 쓰면 효율적이다.
useMemo는 콜백함수를 넣어주면 된다. 두번째 파라미터는 useEffect처럼 호출될 함수에 들어갈 매개변수를 적어주면 될 것 같다.
● useCallback
아까 useMemo에서 Average.js를 보면 이벤트 핸들러로 달아주었던 onChange와 onInsert는 컴포넌트가 렌더링 될 때마다 새로운 만들어진 함수를 사용하게 됩니다. 따라서 이는 효율적이지 못하므로 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야할 컴포넌트의 개수가 많아지면 최적화해주는게 좋습니다.
const onChange = useCallback(e=>{
setNumber(e.target.value);
}, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성
const onInsert = useCallback(()=>{
const nextList = [...list, parseInt(number)];
setList(nextList);
setNumber('');
},[number, list]) // number 혹은 list가 바뀌었을 때만 함수 생성
useCallback의 첫번째 파라미터에는 생성하고 싶은 함수를 넣고, 두번째 파라미터에는 배열을 넣어주면 됩니다. 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시해야합니다.
onChange처럼 비어있는 배열을 넣게 되면 컴포넌트가 렌더링되었을 때 만들었던 함수를 계속해서 재사용하게 되면 onInsert처럼 배열에 list와 number을 넣게 되면 내용이 바뀌거나 새로운 항목이 추가될 때 새로 만들어진 함수를 사용하게 됩니다.
● useRef
useRef는 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해줍니다.
Average.js
import { useState, useMemo, useCallback, useRef } from 'react';
const getAverage = (numbers)=>{
console.log("평균값 계산중...")
if(numbers.length === 0) return 0;
const sum = numbers.reduce((acc, cur)=> acc+ cur);
return sum / numbers.length;
}
const Average = ()=>{
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const inputEl = useRef(null);
const onChange = useCallback(e=>{
setNumber(e.target.value);
}, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성
const onInsert = useCallback(()=>{
const nextList = [...list, parseInt(number)];
setList(nextList);
setNumber('');
inputEl.current.focus();
},[number, list]) // number 혹은 list가 바뀌었을 때만 함수 생성
const avg = useMemo(()=> getAverage(list), [list]);
return(
<div>
<input value={number} onChange={onChange} ref={inputEl}/>
<button onClick={onInsert}>등록</button>
<ul>
{
list.map((value, idx)=> <li key={idx}>{value}</li>)
}
</ul>
<div>
<b>평균값 :</b> {avg}
</div>
</div>
)
}
export default Average;