업데이트:

태그:

카테고리:



Velopert



Hook이 도입된 이유, 역사

  • Hooks는 리액트 16.8v에서 새로 도입된 기능이다.
  • 기존에 클래스형 컴포넌트에서 state를 사용할때, 컴포넌트간 복잡한 관계로 인한 로직 재사용이 힘들고,
  • 이해하기 어렵고,
  • 클래스형 컴포넌트에서 이벤트를 달아줄때, this를 binding해줘야하는 다양한 이슈들이 있었다.
  • 이를 해결하기 위해서 Hooks가 도입되었다.


  • 함수형 컴포넌트에서 state를 사용한다는 것은 리액트가 함수형 프로그래밍이 된다는 것을 의미한다.


Hooks는 recompose에서 시작되었다. 이 라이브러리가 Hooks의 기본적인 개념들을 처음으로 다뤘는데, 이 라이브러리는 리액트 팀에의해 인수되었다.



useState

Hooks의 도입으로 함수형 컴포넌트에서 state의 사용이 가능하다.

import { useState } from "react";

const Counter = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <p>현재 카운터값은 {value}입니다.</p>
      <button onClick={() => setValue(value + 1)}> + 1</button>
      <button onClick={() => setValue(value - 1)}> - 1</button>
    </div>
  );
};

export default Counter;

useState를 import한다.
useState는 기본적으로 2개의 원소를 가지는 배열을 리턴한다.
첫 번째 원소는 state를 유지할 값,
두 번째 원소는 값을 변경할 함수이다.

비구조화 할당 문법을 사용하면 좀 더 직관적으로 원소들을 변수에 할당할 수 있다.


여러개의 state 사용

  • state를 유지해야하는 값이 여러개라면 useState를 여러개 사용하면 된다.



useEffect

  • useEffect를 사용하면 함수형 컴포넌트에서 side effect를 실행할 수 있다.

  • 즉, useEffect는 클래스형 컴포넌트의 라이프 사이클 메서드를 대체할 수 있다.
  • componentDidMount, componentDidUpdate, componentWillUnmount가 합쳐진 것으로 생각해도 좋다(리액트 공식문서)


  • 컴포넌트가 첫 렌더링(마운트) 되었을때,
  • 업데이트 되었을때,
  • 그리고 DOM에서 사라지기 전(언마운트)될 때를 관리할 수 있다.


useEffect는 기본적으로 콜백함수와 배열을 인자로 가지는데,

  • 콜백함수는 useEffect가 호출되면 실행할 함수,
  • 배열은 호출할때 감시할 state이다


예시

useEffect(() => {
  console.log("렌더링이 완료되었습니다.");
  console.log(name, nickName);
});

이렇게 사용하는게 기본형태라고 볼 수 있는데, useEffect는 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다.

스크린샷 2021-12-12 오전 9 19 22

컴포넌트의 렌더링이 완료되었을때 기본적으로 1회 호출되며, 컴포넌트가 업데이트되어 다시 렌더링될때마다 호출되는 것을 볼 수 있다.

즉 2번째 인자가 없으면 컴포넌트의 state가 변할때마다 호출되므로 componentDidmount & componentDidUpdate의 역할을 한다.


마운트 될때만 사용하기

useEffect를 componentDidMount로 사용하고싶다면,

  • 빈 배열을 2번째 인자로 부여한다.
useEffect(() => {
  console.log("마운트시에만 실행");
  console.log(name, nickName);
}, []);

두번째 인자에 빈 배열을 주면 감시할 state가 없으므로, 컴포넌트가 마운트될때 1번만 호출된다.

따라서 componentDidmount의 역할만을 하게된다.
스크린샷 2021-12-12 오전 9 24 13



특정 값이 업데이트 될때만

useUpdate를 특정값이 업데이트 될때만 사용하고싶다면,

  • 해당 state를 배열에 추가한다.
useEffect(() => {
  console.log("name이 업데이트 되었습니다.");
}, [name]);

스크린샷 2021-12-12 오전 9 27 13

두번째 인자로 들어가는 배열의 인자로 state를 넣으면 첫 마운트에 기본적으로 실행된 이후,

  • 해당 state가 변할때만 실행하도록하는 코드가 된다.



clean-up

위의 예시들과는 달리 정리가 필요한 effect가 있다.
clean-up은 해석하면 정리인데, Unmount 되기전에 하는 작업을 의미한다고 이해했다.

  • setInterval처럼 주기적으로 호출하는 함수가 데이터를 다운로드한다고 생각할때,
  • 다운로드된 데이터를 렌더링하는 컴포넌트가 사라졌는데도 계속 호출하면? 엄청난 메모리 누수가 생길 것이다.

  • clean-up은 라이프사이클 메서드 중, componuntWillUnmount, componentWillUpdate를 대체할 수 있다.

  • 사용법은 useEffect함수에 리턴함수를 설정해주면 된다.

예시로 컴포넌트를 DOM상에서 사라지게하거나 생기게할 수 있는 버튼을 만든다고 생각해보자!

import React from "react";
import { useState } from "react/cjs/react.development";
import Counter from "./components/Counter";
import Info from "./components/Info";

const App = () => {
  const [visible, setVisible] = useState(false);

  const handleClick = () => {
    setVisible(!visible);
  };
  return (
    <div className="App">
      <button onClick={handleClick}>{visible ? "숨기기" : "보이기"}</button>
      {visible && <Info />}
    </div>
  );
};

export default App;

렌더링할 APP컴포넌트가 숨기기/보이기 버튼을 만드는데, visible state에 따라 Info컴포넌트를 마운트하거나, 언마운트한다.

import React, { useState, useEffect } from "react";

const Info = () => {
  const [name, setName] = useState("");
  const [nickName, setNickname] = useState("");
  useEffect(() => {
    console.log("effect");
    console.log(name);
    return () => {
      console.log("clean up");
      console.log(name);
    };
  }, [name]);

  const onChangeName = (e) => {
    setName(e.target.value);
  };

  const onChangeNickname = (e) => {
    setNickname(e.target.value);
  };

  return (
    ...)
};

export default Info;

Info 컴포넌트의 useEffect는 name state를 감시하고있다.

앱을 보면, 초기값으로 현재 Info 컴포넌트가 숨겨져있어 Mount되지 않았다.

스크린샷 2021-12-12 오전 9 45 00

  • 그리고 버튼을 눌러 Info 컴포넌트를 마운트시키면 effect만 콘솔에 출력되는 것을 볼 수 있다.

  • 그리고 다시 숨기면 Info 컴포넌트가 언마운트되면서 cleanup되는 것을 볼 수 있다.

스크린샷 2021-12-12 오전 9 47 16

  • 이 상태에서 이름을 입력하면 name의 상태가 바뀜에따라 effect가 출력되는데,
  • 다음 값을 입력하면 cleanup함수가 먼저 호출된다.(name state가 변경되므로, cleanup먼저 실행)



useReducer

  • 컴포넌트의 다양한 상황에 따라 다양한 상태를 다른 값으로 업데이트하고싶을때 사용하는 훅.

  • 다수의 하윗값을 사용하거나, 다음 state가 이전 state에 의존적인 경우 useReducer를 선호한다.

  • useReducer훅을 사용하면 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있는 장점이 있다.


useReducer로 카운터구현

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}


const [state, dispatch] = useReducer(reducer, initialState);
  • useReducer의 첫번째 인자로 reducer 함수를 넣고, 두번째 인자로 state의 기본값을 넣어준다.
  • 그러면 useReducer는 state값state에 적용할 수 있는 reducer함수를 적용한 dispatch라는 함수를 받아온다.


function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

reducer는 현재상태와 업데이트위한 필요한 정보를 담은 액션값을 전달받아 새로운 상태를 반환하는 함수이다.

reducer또한 불변성을 지켜주어야한다.


<button onClick={() => dispatch({ type: "INCREMENT" })}> + 1</button>

dispatch함수에는 파라미터로 액션값을 객체형태로 넣어주면 reducer함수가 그 액션값의 타입에 맞게 호출된다.



useReducer로 인풋상태관리하기

리듀서를 이용하면 클래스형 컴포넌트에서 state를 여러개 관리하던 것과 비슷한 형태로 할 수 있다.

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>
            <div>
                <input value={name} name="name" onChange={onChange}></input>
                <input value={nickName} name="nickName" onChange={onChange}></input>
            </div>
      ...
    </div>)
  • reducer함수는 리턴값으로 state를 업데이트하는데,
    • dispatch로 action인자가 들어오면
      • 불변성을 지키기위해 spread문법으로 기존 state를 풀고,
      • [action.name]: action.value -> e.target.name: e.target.value로 기존 state를 덮어씌운다.



useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 메모이제이션된 값을 리턴한다.

  • 함수와 dependecies 배열(감시할 값의 배열)을 넣어야한다.

평균값을 계산하는 예시를 만들자.

useMemo를 사용해야할때

import { useState } from "react/cjs/react.development";

const getAverage = (numbers) => {
  console.log("계산중...");

  if (numbers.length === 0) {
    //빈 리스트일때
    return 0;
  }
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]); //배열state
  const [number, setNumber] = useState(""); //numberstate

  const onChange = (e) => {
    setNumber(e.target.value); //number state 변화
  };
  const onInsert = (e) => {
    const nextList = list.concat(parseInt(number)); //불변성
    setList(nextList);
    setNumber("");
  };
  return (
    <div>
      <input value={number} onChange={onChange}></input>
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {getAverage(list)}
      </div>
    </div>
  );
};

export default Average;
  • 리액트는 state가 변함에따라 컴포넌트를 다시 렌더링한다.
  • 이 코드의 문제점은 onChange에 의해 number state가 변함에따라 컴포넌트가 다시 렌더링되고,
    • 이에따라 getAverage(list)함수를 호출한다는 것이다.
  • 값을 입력중임에도 불필요한 계산을 계속하기때문에, 이런 불필요한 메모리낭비를 막기위해서 useMemo를 사용한다.


useMemo사용 예시

import { useState } from "react/cjs/react.development";

const getAverage = (numbers) => {
  console.log("계산중...");

  if (numbers.length === 0) {
    //빈 리스트일때
    return 0;
  }
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]); //배열state
  const [number, setNumber] = useState(""); //numberstate

  const onChange = (e) => {
    setNumber(e.target.value); //number state 변화
  };
  const onInsert = (e) => {
    const nextList = list.concat(parseInt(number)); //불변성
    setList(nextList);
    setNumber("");
  };
  const avg = useMemo(() => getAverage(list), [list]);
  return (
    <div>
      <input value={number} onChange={onChange}></input>
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {avg}
      </div>
    </div>
  );
};

export default Average;
  • onChange에 의해 state가 일괄적으로 update된 후, avg가 호출되는데,
  • state중 list를 감시할 값으로 두었기때문에, list state가 변경되었을때(업데이트됐을때)만 getAverage함수를 호출해 계산한다.

  • state를 업데이트할때 불변성을 지켜야한다고 배웠다.
  • 불변성법칙을 준수한다는 것은, setList(nextList)가 다른 메모리값을 만들어 내는 것이다.

  • list의 메모리값이 그대로라는 것은 개발자가 list state를 변경하지 않았으므로, useMemo는 기존에 연산된 값을 리턴할 것이다.

  • list의 메모리값이 다르다는 것은 onInsert불변성법칙을 준수하면서 새로운 리스트를 반환했기 때문에
    • useMemo는 감시중인(의존성값인)list의 메모리값이 다르다는 것을 인식하고, getAverage를 호출할 것이다.



useCallback

useMemo가 특정 값을 메모이제이션한다면

  • useCallback은 함수를 메모이제이션한다.

  • 반복적으로 선언되는 함수를 메모이제이션하는 것인데,
  • 함수를 선언하는 것 자체가 큰 메모리나 cpu리소스를 차지하는 작업은 아니다.
  • 그럼에도 컴포넌트의 렌더링을 자주 해줘야하거나, 렌더링할 컴포넌트의 깊이가 깊어지면 이 부분을 최적화해줘야한다.

위 함수에서 이벤트함수인 onChangeonInsert에 적용한다.

const onChange = useCallback((e) => {
  setNumber(e.target.value); //number state 변화
}, []);

const onInsert = useCallback(
  (e) => {
    const nextList = list.concat(parseInt(number)); //불변성
    setList(nextList);
    setNumber("");
  },
  [number, list]
);

onChange는 Number state의 현재값과는 상관없이 값을 덮어서 업데이트만하기때문에 의존성배열(두번째인자)에 감시할 값이 필요하지 않다.

하지만 onInsertcurrent numberlist를 기준으로 값을 설정하는 함수이므로 의존성배열에 number와 list를 필요로한다.



useRef

useRef는 2가지의 용도로 사용된다.


DOM에 접근하는 용도

input으로 focus가 자연스럽게 넘어가도록 할 때, ref를 DOM에 달아주었는데, useRef를 사용하면 더 쉽게 ref를 사용할 수 있다.


  ...
const Average = () => {
  const [list, setList] = useState([]); //배열state
  const [number, setNumber] = useState(""); //numberstate
  const inputE1 = useRef(null);

...
const onInsert = useCallback(
  (e) => {
    const nextList = list.concat(parseInt(number)); //불변성
    setList(nextList);
    setNumber("");
    inputE1.current.focus()
  },
  [number, list]
);
...

return (
  <div>
    <input value={number} onChange={onChange} ref={inputE1}></input>
    ...
)

export default Average;

inputE1이라는 변수에 useRef를 선언한다.
ref를 달아줄 태그에 ref props로 inputE1을 달아주고,
버튼이 클릭될때마다 onInsert될때마다 input으로 focus가 자동으로 넘어가게끔 inputE1.current.focus()를 달아준다.


로컬변수 사용

useRef는 로컬변수를 관리하는 훅으로 쓰인다고하는데,
로컬변수란 컴포넌트 렌더링과 관계없이 사용할 변수를 의미한다.


클래스형 컴포넌트에서는 가변값을 유지하기위해 인스턴스필드에서 선언했는데,

  • 함수형 컴포넌트에서는 useRef()를 통해 가변값을 유지한다.

useRef는 순수 자바스크립트 객체를 생성하기때문에 매번 렌더링 할 때,

  • 동일한 ref객체를 만들어준다.

컴포넌트에서 state로 선언된 변수는 세터함수를 호출하고, 렌더링된 이후로 상태를 조회할 수 있지만

  • useRef로 관리되는 변수는 설정 이후 바로 조회할 수 있다.

이 변수로 setTimeout, setInterval로 만들어진 id나, scroll위치 등의 값을 관리한다.

useRef를 사용하는 방법으로는 useRef로 선언된 값을 바꾸고싶다면 .current로 접근해 값을 조회하거나, 수정하면 된다.

  • 단, .current로 값을 바꾼다고하더라도 리렌더링을 유발하지는 않는다.(당연하지,,state가 아니니깐…)
  • useRef로 관리하는 변수는 값이 바뀐다고해서 컴포넌트가 리렌더링되지 않는다.
import React, { useRef } from "react";

const RefSample = () => {
  const id = useRef(1);
  const setId = (n) => {
    id.current = n;
  };
  const printId = () => {
    console.log(id.current);
  };
  return <div>refsample</div>;
};
export default RefSample;



커스텀 Hooks 만들기

  • 여러 컴포넌트에서 쓰이는 로직을 함수로 뽑아내기

  • input은 value에 props로 전달된 state값을 업데이트하기위해 name을 주고,
  • onChange에서 setValue([e.target.name] : e.target.value) 이런 방식으로 value값을 업데이트했다.

  • 이 로직이 input에서 반복되는 것을 알고 있다면, 함수화해서 사용하는 것이 더 편하다. 이럴때 커스텀 Hooks를 만든다.

input에서 쓰인 useReducer로직을 함수화한다.

import { useReducer } from "react";

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value,
  };
}

export default function useInputs(initialForm) {
  const [state, dispatch] = useReducer(reducer, initialForm);
  const onChange = (e) => {
    dispatch(e.target);
  };
  return [state, onChange];
}



책 + 인터넷자료을 뒤져가면서 이해한걸 정리했는데, 아직 미숙한 것 같다.
Hook들이 컴포넌트 최적화에 초점이 맞춰져있는 만큼, 미니 프로젝트를 생성하고, 이를 최적화해가면서 사용하면서 공부하는게 더 좋을 듯 싶다.

댓글남기기