First written: 22-12-20
Uploaded: 22-12-28
Last modified: 23-03-01
Hooks는 React v16.8에 도입되었다. Functional Component에서 이전에는 할 수 없었던 작업들을 지원해주기 위해 도입되었다. 이번 글에서는 hooks 전부를 살펴보기보다는 Functional lifecycle에 관여하는 Hooks에 관해서 살펴볼 것이다.
따라치고자 한다면 App.js 내 코드를 아래와 같이 바꿔주자.
(src/App.js)
import Hooks from './Hooks';
const App = () => {
return (
<Hooks />
);
}
export default App;
useState
는 이전 글에서도 자주 보였던 hooks라서 어렵지 않을 것이다.
(src/Hooks.js)
import { useState } from 'react';
const Hooks = () => {
const [name, setName] = useState('');
const onChangeName = e => {
setName(e.target.value);
}
return (
<div>
<input value={name} onChange={onChangeName} />
<p>name: {name}</p>
</div>
)
}
export default Hooks;
const [name, setName] = useState('');
형태로 사용하며, useState()
의 parameter로 전달되는 것이 initial value다. 해당 값에 접근하고 싶을 때는 name
, 해당 값을 조정해주고 싶을 때는 setName
을 사용한다.
Component가 rendering될 때마다 특정 작업을 수행할 수 있게 해준다.
(src/Hooks.js)
import { useState, useEffect } from 'react';
const getRandomColorCode = () => {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
const Hooks = () => {
const [name, setName] = useState('');
const [color, setColor] = useState('#000000');
useEffect(() => {
console.log('mounted');
}, [])
useEffect(() => {
console.log('name changed')
setColor(getRandomColorCode());
return () => {
console.log('cleanup part');
console.log(name);
}
}, [name]);
const onChangeName = e => {
setName(e.target.value);
}
const style = {
color: color
}
return (
<div>
<input value={name} onChange={onChangeName} />
<p>name: <span style={style}>{name}</span></p>
</div>
)
}
export default Hooks;
첫 번째 인자로는 rendering될 때마다 실행할 함수가 들어간다.
두 번째 인자로 아무것도 주지 않으면 rendering될 때마다 첫 번째 인자의 함수가 실행된다. 두 번째 인자로 빈 array를 주면 mount될 때만 실행된다. 두 번째 인자로 state가 들어있는 array를 주면 array 안에 들어있는 state가 변할 때마다 호출된다.
첫 번째 callback 함수에서 함수를 return해주면 clean-up 함수를 실행시킬 수 있다. clean-up 함수는 re-rendering이후에 effect를 새로 주기 전에 동작하게 될 것이다. 즉, re-rendering => clean-up => effect 순으로 호출된다는 이야기다. 주로 이전에 있었던 side effect(특히 subscription)를 제거해주는 용도로 사용된다. 하여 console.log(name)
에서 name
은 이전의 상태를 보여주게 된다.
이를 활용한 예시 중 위의 것보다 더 그럴듯한 예시는 setTimeout이 쓰인 아래의 코드일 것이다.
const [timer, setTimer] = useState(false);
useEffect(() => {
let myTimer = setTimeout(() => setTimer(true), 1000);
return () => {
clearTimeout(myTimer);
};
}, []);
아래의 코드 다섯 줄 모두 어떻게 동작할지 예측이 된다면 이론적으로 useEffect를 충분히 이해했다 할 수 있다. 체크해보자.
useEffect( () => console.log("called for every update") );
useEffect( () => console.log("called when mounted"), [] );
useEffect( () => console.log("called when data updated"), [data] );
useEffect( () => () => console.log("called when unmounted"), [] );
useEffect( () => () => console.log("called when data updated or unmounted"), [data] );
useState
와 비슷하게 state를 update할 때 사용하나, 좀 더 다양한 상황을 다루고 세세하게 다루고 싶을 때 사용하게 된다. Reducer는 update type을 규정해줄 action value를 전달받아 새로운 value를 return하는 함수라는 것을 알고 있었다면, 이 개념이 그리 어렵게 느껴지진 않을 것이다.
아직 무슨 말인지 모르겠더라도 좋다. 아래의 코드를 보자.
(src/Hooks.js)
import { useState, useEffect, useReducer } from 'react';
const getRandomColorCode = () => {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
const numberReducer = (state, action) => {
switch(action.type){
case 'INCREMENT':
return { value: state.value +1 }
case 'DECREMENT':
return { value: state.value -1 }
case 'DOUBLE':
return { value: state.value *2 }
default:
return state;
}
}
const inputReducer = (state, action) => {
return {
...state,
[action.name]: action.value
}
}
const Hooks = () => {
const [name, setName] = useState('');
const [color, setColor] = useState('#000000');
const [number, dispatchNumber] = useReducer(numberReducer, {value: 0});
const [inputState, dispatchInputState] = useReducer(inputReducer, {message: '', addressee: ''});
useEffect(() => {
console.log('mounted');
}, []);
useEffect(() => {
console.log('name changed')
setColor(getRandomColorCode());
return () => {
console.log('cleanup part');
console.log(name);
}
}, [name, number.value]);
const onChangeName = e => {
setName(e.target.value);
}
const onChange = e => {
dispatchInputState(e.target);
}
const style = {
color: color
}
const { message, addressee } = inputState;
return (
<div>
<input value={name} onChange={onChangeName} />
<p>name: <span style={style}>{name}</span></p>
<hr />
<p>number: <span style={style}>{number.value}</span></p>
<button onClick={() => dispatchNumber({type: 'INCREMENT'})}>+1</button>
<button onClick={() => dispatchNumber({type: 'DECREMENT'})}>-1</button>
<button onClick={() => dispatchNumber({type: 'DOUBLE'})}>*2</button>
<hr />
<input name="addressee" value={addressee} placeholder="addressee" onChange={onChange} />
<input name="message" value={message} placeholder="message" onChange={onChange} />
<p><b>To {addressee}</b>, "{message}"</p>
</div>
)
}
export default Hooks;
const [number, dispatchNumber] = useReducer(numberReducer, {value: 0});
와 같은 형태로 작성한다. useReducer의 첫 번째 parameter로는 reducer 함수가, 두 번째로는 초기값이 들어가게 된다. 해당 값에 접근하고 싶을 때는 number에, 해당 값을 수정하고 싶다면 dispatchNumber 함수에 접근하면 된다.
numberReducer처럼 꼭 action 객체에 type field가 있어야 하는 것은 아니다. useReducer에서 action은 객체뿐 아니라 어떤 값도 사용이 가능해서 inputReducer와 같은 함수로 input onChange 함수를 재사용하는 것도 가능하다.
(...)
const getLuckyNumber = (num) => {
console.log('calculating lucky number');
return num * 7;
}
(...)
return (
(...)
<p>Your lucky number: {getLuckyNumber(number.value)}</p>
(...)
)
(...)
만일 우리가 사용자가 클릭하여 얻은 number에 대하여(이전 코드 참고) lucky number를 생성해주기로 했다고 가정해보자. 그렇다면 위와 같이 코드를 작성할 수 있다. 하지만 이 과정에서 문제가 생기는데, 다른 input의 값들을 조정할 때조차 getLuckyNumber 함수가 실행된다는 것이다. 우리는 특정 값(여기서는 number.value)이 변할 때만 이 함수가 작동하도록 아래처럼 조정해주어야 할 것 이다. 그럴 때 사용하는 것이 useMemo다.
(src/Hooks.js)
import { useState, useEffect, useReducer, useMemo } from 'react';
const getRandomColorCode = () => {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
const numberReducer = (state, action) => {
switch(action.type){
case 'INCREMENT':
return { value: state.value +1 }
case 'DECREMENT':
return { value: state.value -1 }
case 'DOUBLE':
return { value: state.value *2 }
default:
return state;
}
}
const inputReducer = (state, action) => {
return {
...state,
[action.name]: action.value
}
}
const Hooks = () => {
const [name, setName] = useState('');
const [color, setColor] = useState('#000000');
const [number, dispatchNumber] = useReducer(numberReducer, {value: 0});
const [inputState, dispatchInputState] = useReducer(inputReducer, {message: '', addressee: ''});
useEffect(() => {
console.log('mounted');
}, []);
useEffect(() => {
console.log('name changed')
setColor(getRandomColorCode());
return () => {
console.log('cleanup part');
console.log(name);
}
}, [name, number.value]);
const onChangeName = e => {
setName(e.target.value);
}
const onChange = e => {
dispatchInputState(e.target);
}
const style = {
color: color
}
const { message, addressee } = inputState;
const luckyNumber = useMemo(() => {
console.log('calculating lucky number');
return number.value * 7;
}, [number.value]);
return (
<div>
<input value={name} onChange={onChangeName} />
<p>name: <span style={style}>{name}</span></p>
<hr />
<p>number: <span style={style}>{number.value}</span></p>
<button onClick={() => dispatchNumber({type: 'INCREMENT'})}>+1</button>
<button onClick={() => dispatchNumber({type: 'DECREMENT'})}>-1</button>
<button onClick={() => dispatchNumber({type: 'DOUBLE'})}>*2</button>
<p>Your lucky number: {luckyNumber}</p>
<hr />
<input name="addressee" value={addressee} placeholder="addressee" onChange={onChange} />
<input name="message" value={message} placeholder="message" onChange={onChange} />
<p><b>To {addressee}</b>, "{message}"</p>
</div>
)
}
export default Hooks;
useMemo로 함수를 덮어주고, 두 번째 인자로 어떤 state가 변할 때에만 호출될지를 적어주었다.
(...)
const luckyNumber = useMemo(() => {
console.log('calculating lucky number');
return number.value * 7;
}, [number.value]);
(...)
현재 코드를 보면 onChangeName
함수와 onChange
함수는 re-rendering이 이루어질 때마다 매번 선언되는 것을 알 수 있다. 작은 프로젝트에서는 이런 부하가 부담될 리 없겠지만, 프로젝트 규모가 커지기 전에 보수해야하는 부분임에는 틀림이 없다. 그럴 때 useCallback을 써서 언제 함수를 생성해야 하는지 결정해줄 수 있다.
(src/Hooks.js)
import { useState, useEffect, useReducer, useMemo, useCallback } from 'react';
const getRandomColorCode = () => {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
const numberReducer = (state, action) => {
switch(action.type){
case 'INCREMENT':
return { value: state.value +1 }
case 'DECREMENT':
return { value: state.value -1 }
case 'DOUBLE':
return { value: state.value *2 }
default:
return state;
}
}
const inputReducer = (state, action) => {
return {
...state,
[action.name]: action.value
}
}
const Hooks = () => {
const [name, setName] = useState('');
const [color, setColor] = useState('#000000');
const [number, dispatchNumber] = useReducer(numberReducer, {value: 0});
const [inputState, dispatchInputState] = useReducer(inputReducer, {message: '', addressee: ''});
useEffect(() => {
console.log('mounted');
}, []);
useEffect(() => {
console.log('name changed')
setColor(getRandomColorCode());
return () => {
console.log('cleanup part');
console.log(name);
}
}, [name, number.value]);
const onChangeName = useCallback(e => {
setName(e.target.value);
}, []);
const onChange = useCallback(e => {
dispatchInputState(e.target);
}, []);
const style = {
color: color
}
const { message, addressee } = inputState;
const luckyNumber = useMemo(() => {
console.log('calculating lucky number');
return number.value * 7;
}, [number.value]);
return (
<div>
<input value={name} onChange={onChangeName} />
<p>name: <span style={style}>{name}</span></p>
<hr />
<p>number: <span style={style}>{number.value}</span></p>
<button onClick={() => dispatchNumber({type: 'INCREMENT'})}>+1</button>
<button onClick={() => dispatchNumber({type: 'DECREMENT'})}>-1</button>
<button onClick={() => dispatchNumber({type: 'DOUBLE'})}>*2</button>
<p>Your lucky number: {luckyNumber}</p>
<hr />
<input name="addressee" value={addressee} placeholder="addressee" onChange={onChange} />
<input name="message" value={message} placeholder="message" onChange={onChange} />
<p><b>To {addressee}</b>, "{message}"</p>
</div>
)
}
export default Hooks;
onChangeName
과 onChange
는 컴포넌트가 처음 렌더링될 때만 함수를 생성하도록 했다.
만일 아래와 같이 number.value가 변할 때마다 해당 값을 가지고 어떤 list를 만들고 있었다면, number.value와 list가 변할 때 함수가 생성되도록 해주면 된다.
const changeList = useCallback(() => {
setList({...list, parseInt(number.value)});
}, [number.value, list]);
함수 component에서 ref를 사용할 수 있게 해주는 hook이다. ref는 예전 글에서 다뤘던 내용이다. 다시 말하자면, 특정 DOM element를 찝어서 해당 element에 어떤 조작을 가하고 싶을 때 마치 Javascript의 document.querySelector
메서드와 비슷한 역할을 하는 친구다.
(src/Hooks.js)
import { useState, useEffect, useReducer, useMemo, useCallback, useRef } from 'react';
const getRandomColorCode = () => {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
const numberReducer = (state, action) => {
switch(action.type){
case 'INCREMENT':
return { value: state.value +1 }
case 'DECREMENT':
return { value: state.value -1 }
case 'DOUBLE':
return { value: state.value *2 }
default:
return state;
}
}
const inputReducer = (state, action) => {
return {
...state,
[action.name]: action.value
}
}
const Hooks = () => {
const [name, setName] = useState('');
const [color, setColor] = useState('#000000');
const [number, dispatchNumber] = useReducer(numberReducer, {value: 0});
const [inputState, dispatchInputState] = useReducer(inputReducer, {message: '', addressee: ''});
const defaultInputEl = useRef(null);
useEffect(() => {
console.log('mounted');
defaultInputEl.current.focus();
}, []);
useEffect(() => {
console.log('name changed')
setColor(getRandomColorCode());
return () => {
console.log('cleanup part');
console.log(name);
}
}, [name, number.value]);
const onChangeName = useCallback(e => {
setName(e.target.value);
}, []);
const onChange = useCallback(e => {
dispatchInputState(e.target);
}, []);
const style = {
color: color
}
const { message, addressee } = inputState;
const luckyNumber = useMemo(() => {
console.log('calculating lucky number');
return number.value * 7;
}, [number.value]);
return (
<div>
<input value={name} onChange={onChangeName} ref={defaultInputEl} />
<p>name: <span style={style}>{name}</span></p>
<hr />
<p>number: <span style={style}>{number.value}</span></p>
<button onClick={() => dispatchNumber({type: 'INCREMENT'})}>+1</button>
<button onClick={() => dispatchNumber({type: 'DECREMENT'})}>-1</button>
<button onClick={() => dispatchNumber({type: 'DOUBLE'})}>*2</button>
<p>Your lucky number: {luckyNumber}</p>
<hr />
<input name="addressee" value={addressee} placeholder="addressee" onChange={onChange} />
<input name="message" value={message} placeholder="message" onChange={onChange} />
<p><b>To {addressee}</b>, "{message}"</p>
</div>
)
}
export default Hooks;
useRef(null)
에서 값을 받아 변수에 저장한 뒤에 원하는 DOM element에 ref={해당 변수}
를 넣어준다. 그 뒤에 해당 변수.current
로 DOM element에 접근해 해당 element를 직접 조작할 수 있다. 우리 예시에서는 mount가 된 순간 name input에 focus가 가도록 설정해보았다.
(...)
const defaultInputEl = useRef(null);
useEffect(() => {
console.log('mounted');
defaultInputEl.current.focus();
}, []);
(...)
return (
(...)
<input value={name} onChange={onChangeName} ref={defaultInputEl} />
(...)
)
(...)
참고 서적 :
김민준, 2019, 길벗, 『리액트를 다루는 기술, 개정판』