리액트 Summary - 2-2. 함수형 라이프사이클(Hooks) 총정리-thumbnail

리액트 Summary - 2-2. 함수형 라이프사이클(Hooks) 총정리

Functional Component Lifecycle(Hooks) summary
286

Hooks

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

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을 사용한다.

useEffect

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] );

useReducer

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 함수를 재사용하는 것도 가능하다.

useMemo, useCallback

useMemo

(...)

  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]);
(...)

useCallback

현재 코드를 보면 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;

onChangeNameonChange는 컴포넌트가 처음 렌더링될 때만 함수를 생성하도록 했다.

만일 아래와 같이 number.value가 변할 때마다 해당 값을 가지고 어떤 list를 만들고 있었다면, number.value와 list가 변할 때 함수가 생성되도록 해주면 된다.

const changeList = useCallback(() => {
  setList({...list, parseInt(number.value)});
}, [number.value, list]);

useRef

함수 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, 길벗, 『리액트를 다루는 기술, 개정판』