리액트

(리액트를 다루는 기술) 8장 Hooks

세모이 2021. 4. 8. 13:53
반응형

Hooks

  • Hooks는 리액트 v16.8에 새로 도입된 기능으로 함수형 컴포넌트에서도 상태 관리를 할 수 있는
    useState, 렌더링 직후 작업을 설정하는 useEffect 등의 기능을 제공
  1. useState

useState는 가장 기본적인 Hook이며, 함수형 컴포넌트에서도 가변적인 상태를 지닐 수 있게 해줌.

App.js;

import React from "react";
import Counter from "./Counter";

const App = () => {
  return (
    <div>
      <Counter />
    </div>
  );
};

export default App;
Counter.js;

import React, { useState } from "react";

const Counter = () => {
  const [value, setValue] = useState(0); //첫번째 원소는 상태 값, 두번째 원소는 상태를 설정하는 함수
  return (
    <div>
      <p>
        현재 카운터값은<b>{value}</b>입니다.
      </p>
      <button onClick={() => setValue(value + 1)}>+1</button>
      <button onClick={() => setValue(value - 1)}>-1</button>
    </div>
  );
};

export default Counter;
  • 관리할 상태가 여러개인 것도 useState로 편하게 관리 가능
import React, { useState } from "react";

const Info = () => {
  const [name, setName] = useState("");
  const [nickname, setNickname] = useState("");

  const onChangeName = (e) => {
    setName(e.target.value);
  };
  const onChangeNickname = (e) => {
    setNickname(e.target.value);
  };
  return (
    <div>
      <div>
        <input value={name} onChange={onChangeName} />
        <input value={nickname} onChange={onChangeNickname} />
      </div>
      <div>
        <div>
          <b>이름:</b>
          {name}
        </div>
        <div>
          <b>닉네임:</b>
          {nickname}
        </div>
      </div>
    </div>
  );
};

export default Info;

  1. useEffect

    useEffect는 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정(componentDidMount와 componentDidUpdate를 합친 형태와 비슷)

Info.js

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

const Info = () => {
  const [name, setName] = useState("");
  const [nickname, setNickname] = useState("");

  useEffect(() => {
    console.log("렌더링이 완료되었습니다"); //렌더링 될때마다 나타남
    console.log({
      name,
      nickname,
    });
  });

  const onChangeName = (e) => {
    setName(e.target.value);
  };
  const onChangeNickname = (e) => {
    setNickname(e.target.value);
  };
  return (
    <div>
      <div>
        <input value={name} onChange={onChangeName} />
        <input value={nickname} onChange={onChangeNickname} />
      </div>
      <div>
        <div>
          <b>이름:</b>
          {name}
        </div>
        <div>
          <b>닉네임:</b>
          {nickname}
        </div>
      </div>
    </div>
  );
};

export default Info;
  • 함수의 두 번째 파라미터로 비어 있는 배열을 넣어주면 처음 렌더링될 때만 실행, 업데이트 될때는 실행X
useEffect(() => {
  console.log("렌더링이 완료되었습니다");
  console.log({
    name,
    nickname,
  });
}, []);
  • 특정 값이 업데이트될 때만 실행하고 싶을 때는 두 번째 파라미터에 검사하고 싶은 값을 넣어주면 된다
useEffect(() => {
  console.log("렌더링이 완료되었습니다");
  console.log({
    name,
    nickname,
  });
}, [name]);
  • 컴포넌트가 언마운트되기 전이나 업데이트되기 직전에 어떤 작업을 수행하고 싶다면 뒷정리(cleanup) 함수를 반환해주어야 한다
useEffect(() => {
  console.log("effect");
  console.log(name);
  return () => {
    console.log("cleanup");
    console.log(name);
  };
});
App.js;

import React, { 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;
  • 렌더링 될때마다 뒷정리 함수가 계속 나타나는데 언마운트될 때만 뒷정리 함수 호출하고 싶다면 useEffect 함수의 두 번째 파라미터에 비어 있는 배열을 넣으면 된다
useEffect(() => {
  console.log("effect");
  console.log(name);
  return () => {
    console.log("cleanup");
    console.log(name);
  };
}, [name]); //-> name 넣으면 계속 나타남

  1. useReducer

    useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트해 주고 싶을 때 사용하는 Hook이다

  • 리듀서 : 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션(action) 값을 전달받아 새로운 상태를 반환하는 함수(새로운 상태를 만들 때는 불변성 지켜야 함)
function reducer(state, action){
    return { ... }; //불변성을 지키면서 업데이트한 새로운 상태를 반환
}
{
    type:'INCREMENT', //액션 값은 주로 이런 형태
}
  • Counter.js로 예제보기

useReducer의 첫 번째 파라미터에는 리듀서 함수를 넣고, 두 번째 파라미터에는 해당 리듀서의 기본값을 넣어준다.

import React, { useReducer } from "react";

function reducer(state, action) {
  //action.type에 따라 다른 작업 수행
  switch (action.type) {
    case "INCREMENT":
      return { value: state.value + 1 };
    case "DECREMENT":
      return { value: state.value - 1 };
      dafault: return state;
  }
}

const Counter = () => {
  const [state, dispach] = useReducer(reducer, { value: 0 });    //state(현재 가리키고 있는 상태) 값과 dispach(액션을 발생시키는 함수) 함수를 받아옴
  return (
    <div>
      <p>
        현재 카운터값은<b>{state.value}</b>입니다.
      </p>

      <button onClick={() => dispach({ type: "INCREMENT" })}>+1</button>
      <button onClick={() => dispach({ type: "DECREMENT" })}>-1</button>
    </div>
  );
};

export default Counter;
  • Info.js 예제
Info.js;

import React, { useEffect, 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>
      <div>
        <input name="name" value={name} onChange={onChange} />
        <input name="nickname" value={nickname} onChange={onChange} />
      </div>
      <div>
        <div>
          <b>이름:</b>
          {name}
        </div>
        <div>
          <b>닉네임:</b>
          {nickname}
        </div>
      </div>
    </div>
  );
};

export default Info;

  1. useMemo

useMemo를 사용하면 함수형 컴포넌트 내부에서 발생하는 연산을 최적화 할 수 있다.

App.js;

import React from "react";
import Average from "./Average";

const App = () => {
  return (
    <div>
      <Average />
    </div>
  );
};

export default App;
Average.js;

import React, { useState } from "react";

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([]);
  const [number, setNumber] = useState("");

  const onChange = (e) => {
    setNumber(e.target.value);
  };
  const onInsert = (e) => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  };

  return (
    <div>
      <input value={number} onChange={onChange} />
      <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;

-> 등록 뿐 아니라 인풋 내용이 수정될 때도 getAverage 함수가 호출 됨(낭비)

Average.js;

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

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([]);
  const [number, setNumber] = useState("");

  const onChange = (e) => {
    setNumber(e.target.value);
  };
  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} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b>
        {avg}
      </div>
    </div>
  );
};

export default Average;

-> 이제는 list 배열의 내용이 바뀔 때만 getAverage 함수가 호출


  1. useCallback

렌더링 성능을 최적화해야 하는 상황에 사용

  • Average.js에서 onChange와 onInsert 함수가 있는데 리렌더링 될때마다 새로 생성됨

    -> 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야 할 컴포넌트 개수가 많아지면 최적화 해주는게 좋음
Average.js;

import React, { useCallback, useMemo, useState } from "react";

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([]);
  const [number, setNumber] = useState("");

  const onChange = useCallback((e) => {
    setNumber(e.target.value);
  }, []); //두번째 파라미터에 비어있는 배열을 넣게되면 컴포넌트가 처음 렌더링될 때만 함수 생성

  const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  }, [number, list]); //두번째 파라미터에 배열을 넣게되면->number 혹은 list가 바뀌었을 때만 함수 생성
  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b>
        {avg}
      </div>
    </div>
  );
};

export default Average;
  • useCallback의 첫 번째 파라미터에는 생성하고 싶은 함수를 넣고, 두 번째 파라미터에는 배열(어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지)을 넣으면됨
  • 배열이 비어 있으면 컴포넌트가 렌더링될 때 한번만 함수가 생성

  1. useRef
  • useRef는 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해줌
Average.js;

import React, { useCallback, useMemo, useState, useRef } from "react";

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([]);
  const [number, setNumber] = useState("");
  const inputEl = useRef(null);

  const onChange = useCallback((e) => {
    setNumber(e.target.value);
  }, []); //컴포넌트가 처음 렌더링될 때만 함수 생성

  const onInsert = useCallback(() => {
    const nextList = list.concat(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, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b>
        {avg}
      </div>
    </div>
  );
};

export default Average;
반응형