📖 리액트/리액트를 다루는 기술

8장 Hooks

놀러와요 버그의 숲 2022. 1. 26. 18:21
728x90
반응형

이 글은 『리액트를 다루는 기술』(개정판/ 김민준 저 / 길벗 출판사)이라는 책을 참고하여 썼습니다.

 

학습 범위:  p190 ~p.215

 

학습목표

1. Hook을 대체 왜 쓰는가?

2. useState와 useEffect는 왜 쓰고, 어떻게 쓰는가?

3. 그외에 Hook들은 개념만 알아두고, 실전에 쓰일 상황에 필요할 때 다시 찾아보자!

 

 

Hooks란?

 

Hooks는 리액트 v16.8에 새로 도입된 기능으로 함수형 컴포넌트에서도 상태 관리를 할 수 있는 useState, 렌더링 직후 작업을 설정하는 useEffect 등의 기능을 제공하여 기존의 함수형 컴포넌트에서 할 수 없었던 다양한 작업을 할 수 있게 해 줍니다.

 

8.1 useState

 

리액트 JS 어플내에서 데이터를 보관하고, 자동으로 리렌더링을 일으킬 수 있는 방법을 배워보려 합니다.

 

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

 

useState는 코드 상단에서 import 구문을 통해 불러오고, 다음과 같이 사용합니다.

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

useState 함수의 파라미터에는 상태의 기본값을 넣어 줍니다.

현재 0을 넣어 주었는데, 결국 카운터의 기본값을 0으로 설정하겠다는 의미입니다.

이 함수가 호출되면 배열을 반환하는데요. 그 배열의 첫 번째 원소는 상태 값, 두 번째 원소는 상태를 설정하는 함수입니다.

이 함수에 파라미터를 넣어서 호출하면 전달받은 파라미터로 값이 바뀌고 컴포넌트가 정상적으로 리렌더링됩니다.

 

(한마디로 이 함수를 이용하면 데이터 값이 바뀌고 컴포넌트도 동시에 리렌더링된다.)

 

tip! useState에서 [ ]는 무엇인가?

구조분해할당 예시

const food = ['pizza','chicken'];

const [myFavFood, mySecondFavFood] = food;

// 여기서 myFavFood는 pizza,  mySecondFavFood는 chicken이 된다.
// 이 행위는 const myFavFood = food[0]
// const mySecondFavFood = food[1] 과 같다.

 

 

예제1. Counter 만들기

import { useState } from "react";

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

  return (
    <>
      <h1>현재 나의 나이는 {value} 입니다.</h1>
      <button onClick={() => setValue(value + 1)}>1년후</button>
      <button onClick={() => setValue(value - 1)}>1년전</button>
    </>
  );
};

export default Counter;

 

예제2. useState를 여러번 사용하기

 

하나의 useState 함수는 하나의 상태 값만 관리할 수 있습니다.

컴포넌트에서 관리해야 할 상태가 여러 개라면 useState를 여러 번 사용하면 됩니다.

 

import { useState } from "react";

const Info = () => {
  const [fruitName, setFruitName] = useState("");
  const [singerName, setSingerName] = useState("");

  const onChangeFruitName = (event) => {
    setFruitName(event.target.value);
  };
  const onChangeSingerName = (event) => {
    setSingerName(event.target.value);
  };
  return (
    <>
      <input
        placeholder="과일을 입력해주세요"
        value={fruitName}
        onChange={onChangeFruitName}
      />
      <input
        placeholder="가수를 입력해주세요"
        value={singerName}
        onChange={onChangeSingerName}
      />
      <p>진실님은 과일 {fruitName}을 좋아한다. </p>
      <p>현수님은 가수 {singerName}를 좋아한다.</p>
    </>
  );
};

export default Info;

 

 

8.2 useEffect

 

리액트는 state가 변화하면 모두 리렌더하기에, 특정 state만 변화를 감지하고 싶을 때 , useEffect를 쓴다.

 

useEffect는 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook입니다.

 

클래스형 컴포넌트의 componentDidMount componentDidUpdate를 합친 형태와 비슷하다고 생각하면 됩니다.

 

useEffect는 두개의 인수를 가지는 함수입니다.

첫번째 인수는 우리가 실행하고 싶은 코드가 들어갑니다.

두번째 들어가는 인수는 dependencies라고 하고, 이는 리액트가 상태 변화를 지켜보고 있다는 것입니다.

그리고 그 상태가 변하면 첫번째 인수에 들어있는 코드를 실행시키게 됩니다.

import { useState, useEffect } from "react";

const InfoUE = () => {
  const [fruitName, setFruitName] = useState("");
  const [singerName, setSingerName] = useState("");

  useEffect(() => {
    console.log("렌더링이 완료되었습니다!");
    console.log({
      fruitName,
      singerName,
    });
  });
  // 컴포넌트가 리렌더링 될 때마다 특정 작업 수행

  const onChangeFruitName = (event) => {
    setFruitName(event.target.value);
  };
  const onChangeSingerName = (event) => {
    setSingerName(event.target.value);
  };
  return (
    <>
      <input
        placeholder="과일을 입력해주세요"
        value={fruitName}
        onChange={onChangeFruitName}
      />
      <input
        placeholder="가수를 입력해주세요"
        value={singerName}
        onChange={onChangeSingerName}
      />
      <p>진실님은 과일 {fruitName}을 좋아한다. </p>
      <p>현수님은 가수 {singerName}를 좋아한다.</p>
    </>
  );
};

export default InfoUE;

 

 

 

8.2.1 마운트 될 때만 실행하고 싶을 때

 

useEffect에서 설정한 함수를 컴포넌트가 화면에 맨 처음 렌더링될 때만 실행하고,

업데이트될 때는 실행하지 않으려면 함수의 두 번째 파라미터로 비어 있는 배열을 넣어 주면 됩니다.

 

 useEffect(() => {
    console.log("마운트 될때만 실행");
  }, []);
  컴포넌트가 화면에 처음 렌더링 될 때만 실행하고, 업데이트 때는 실행하지 않는다.

 

 

8.2.2 특정 값이 업데이트 될 때만 실행하고 싶을 때 

 

 useEffect의 두 번째 파라미터로 전달되는 배열 안에 검사하고 싶은 값을 넣어 주면 됩니다.

 

  useEffect(() => {
    console.log(fruitName);
  }, [fruitName]);
  특정값이 업데이트 될 때만 실행시키고 싶을 때

 

 

8.2.3 뒷정리하기

 

컴포넌트가 언마운트되기 전이나 업데이트되기 직전에 어떠한 작업을 수행하고 싶다면 useEffect에서 뒷정리(cleanup) 함수를

반환해 주어야 합니다.

 

  useEffect(() => {
    console.log("렌더링");
    console.log(fruitName);
    return () => {
      console.log("clean up");
      console.log(fruitName);
    };
  }, [fruitName]);
  
 // 컴포넌트가 언마운트 되기전이나 업데이트 되기 직전에 어떠한 작업을 수행하고 싶다면 cleanup 함수를 반환해야한다.

 

예제1 컴포넌트 숨기기/ 보이기 구현 

 

import { useState } from "react";
import InfoUE from "./InfoUE";

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

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

export default InfoVisible;

 

 

 

 

리액트 공식문서님 말씀

 

다음의 Hook는 이전 섹션에서의 기본 Hook의 변경이거나 특정한 경우에만 필요한 것입니다. 익히는 것에 너무 압박받지는 마세요.

 

 

그러니까 우리모두 압박받지 맙시다 🌱

 

8.3 useReducer

아래와 같이 여러 하위 값이 있는 state를 사용할때 useReducer를 사용하면 좋다. 

 

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

 

장점: 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다는 점

 

개념

 

reducer : state를 업데이트 해주는 역할. (은행과 같다. 우리가 직접 돈을 못 건들듯이 은행을 통해서만 거드릴수있다. )

 

dispatch: state 업데이트를 위한 요구 / 거래내역 업데이트 해달라고 하는 요구 (action이라는 내용이 담긴다 )

 

action:  요구의 내용 / '만원을 출금해주세요' 라는 이 내용 

 

 

import { useReducer } from "react";

// reducer는 현재 상태,그리고 업데이트를 위해 필요한 정보를 담은 action 값을 전달받아 새로운
// 상태로 변환하는 함수이다.
function reducer(state, action) {
  // action type에 따라 다른 작업 수행
  switch (action.type) {
    case "증가":
      return { value: state.value + 1 };
    case "감소":
      return { value: state.value - 1 };
    default:
      return state;
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { value: 0 });
  // useReducer의 첫번째 파라미터에는 리듀서 함수를 넣고, 두번째 파라미터에는 해당 리듀서의 기본값을 넣어줌
  // 이 Hook을 사용하면 state값과 dispatch 함수를 받아옴
  // 여기서 state는 현재 가리키고 있는 상태이고, dispatch는 액션을 발생시키는 함수이다.

  return (
    <div>
      <p>현재 카운터 값은 {state.value}</p>
      <button onClick={() => dispatch({ type: "증가" })}>+1</button>
      <button onClick={() => dispatch({ type: "감소" })}>-1</button>
    </div>
  );
};

export default Counter;

 

 

8.4 useMemo

 

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

 

리액트 공식문서:  useMemo는 의존성이 변경되었을 때에만 메모이제이션된 값만 다시 계산 할 것입니다. 이 최적화는 모든 렌더링 시의 고비용 계산을 방지하게 해 줍니다.

 

어떻게 최적화를 합니까? => 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고, 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용하는 방식입니다.

 

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 함수가 호출되는 것을 확인할 수 있습니다.

인풋 내용이 바뀔 때는 평균값을 다시 계산할 필요가 없는데, 이렇게 렌더링할 때마다 계산하는 것은 낭비겠지요?

 

import React, { useState, useMemo } 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 = () => {
    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;

 

 

8.5 useCallback

 

useCallback useMemo와 상당히 비슷한 함수입니다. 주로 렌더링 성능을 최적화해야 하는 상황에서 사용하는데요.

useCallback을 사용하면 이벤트 핸들러 함수를 필요할 때만 생성할 수 있습니다.

 

useCallback의 첫 번째 파라미터에는 생성하고 싶은 함수를 넣고, 두 번째 파라미터에는 배열을 넣으면 됩니다.

이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시해야 합니다.

 

useCallback(() => {
  console.log(‘hello world!‘);
}, [])

 

useCallback은 결국 useMemo로 함수를 반환하는 상황에서 더 편하게 사용할 수 있는 Hook입니다.

숫자, 문자열, 객체처럼 일반 값을 재사용하려면 useMemo를 사용하고, 함수를 재사용하려면 useCallback을 사용하세요.

 

컴포넌트가 리렌더링될 때마다 함수를 새로 만드는 것이 아니라, 한 번 함수를 만들고 재사용할 수 있도록 useCallback Hook을 사용합니다.

props로 전달해야할 함수를 만들 때는 useCallback을 사용하여 함수를 감싸는 것을 습관화해야 한다.

 

8.6 useRef

 

벨로퍼트님 말씀

 

"JavaScript 를 사용 할 때에는, 우리가 특정 DOM 을 선택해야 하는 상황에 getElementById, querySelector 같은 DOM Selector 함수를 사용해서 DOM 을 선택합니다.

리액트를 사용하는 프로젝트에서도 가끔씩 DOM 을 직접 선택해야 하는 상황이 발생 할 때도 있습니다.

예를 들어서 특정 엘리먼트의 크기를 가져와야 한다던지, 스크롤바 위치를 가져오거나 설정해야된다던지, 또는 포커스를 설정해줘야된다던지 등 정말 다양한 상황이 있겠죠.

추가적으로 Video.js, JWPlayer 같은 HTML5 Video 관련 라이브러리, 또는 D3, chart.js 같은 그래프 관련 라이브러리 등의 외부 라이브러리를 사용해야 할 때에도 특정 DOM 에다 적용하기 때문에 DOM 을 선택해야 하는 상황이 발생 할 수 있습니다.

그럴 땐, 리액트에서 ref 라는 것을 사용합니다."

 

 

1. 저장공간으로 사용됩니다.

 

state의 경우!

state 변화 => 렌더링 => 컴포넌트 내부의 변수들이 초기화

 

(왜냐면 함수형 컴포넌트도 함수이기에 렌더링될때마다 함수를 다시 부르기 때문에! ) 

 

원하지 않는 렌더링 때문에 고생할 때가있다.

 

이럴때 ref안에 값을 저장해두면, 아무리 변경해도 컴포넌트는 다시 렌더링 x => 변수값들 초기화 

 

ex) id 값은 예를들어 화면에 보이지도 않고 이 값이 바뀐다고 해서 컴포넌트가 리렌더링 될 필요도 없다. 단순히 새로운 항목을 만들때 참조 되는 값일 뿐이다.

 

2. DOM요소에 접근

 

마치 바닐라 js에 Document.querySelector 와 유사하다고 합니다.

 

 

8.7. 커스텀 Hooks 만들기

 

여러 컴포넌트에서 비슷한 기능을 공유할 경우, 이를 여러분만의 Hook으로 작성하여 로직을 재사용할 수 있습니다.

 

import React from 'react';
import useInputs from './useInputs';
 
const Info = () => {
  const [state, onChange] = useInputs({
    name: '',
    nickname: ''
  });
  const { name, nickname } = state;
 
  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;

 

 

 

정리 및 요약

 

일단 가장 빈번이 사용되는 useState와 useEffect만 기억하셔도 됩니다!

 

 

다른 Hooks가 궁금하다면?

 

https://ko.reactjs.org/docs/hooks-reference.html#usereducer

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

클래스형 컴포넌트 계속 사용해야 할까?

 

리액트 매뉴얼에 따르면, 기존의 클래스형 컴포넌트는 앞으로도 계속해서 지원될 예정입니다.

그렇기 때문에 만약 유지 보수하고 있는 프로젝트에서 클래스형 컴포넌트를 사용하고 있다면, 이를 굳이 함수형 컴포넌트와 Hooks를 사용하는 형태로 전환할 필요는 없습니다.

다만, 매뉴얼에서는 새로 작성하는 컴포넌트의 경우 함수형 컴포넌트와 Hooks를 사용할 것을 권장하고 있습니다. 앞으로 우리가 프로젝트를 개발할 때는 함수형 컴포넌트의 사용을 첫 번째 옵션으로 두고, 꼭 필요한 상황에서만 클래스형 컴포넌트를 구현하시죠!

 

 

벨로퍼트님 교안 링크

https://react.vlpt.us/basic/01-concept.html

 

1. 리액트는 어쩌다가 만들어졌을까? · GitBook

01. 리액트는 어쩌다 만들어졌을까? 리액트 학습을 본격적으로 하기 전에, 리액트라는 라이브러리가 어쩌다가 만들어졌는지 알면 리액트를 이해하는데 도움이 될 것입니다. JavaScript를 사용하여

react.vlpt.us

 

알게된 점:

useCallback / useMemo

 

함수를 리턴하느냐, 값을 리턴하느냐 차이

 

useEffect /  useCallback

라이프사이클을 위한 훅과 함수를 재사용하기위한 훅차이

 

useCallback( prev=> prev + 1) 그 전값을 아니까 굳이 [ ]안에 써줄 필요 없다.

 

여기서 재사용한다는 의미는 불필요한 재렌더링을 방지한다는 뜻.

 

function App() {
  const [toggle, setToggle] = useState(false);
  const [number, setNumber] = useState(1);

  useEffect(() => {
    console.log("render - number")
  }, [number])

  useEffect(() => {
    console.log("render - boolean")
  }, [toggle])

  const onClick = () => setNumber(number + 1);

	// 이렇게 작성할 경우,  number만 바뀌어도 해당 함수가 재생성됨
  const onToggle = () => setToggle(toggle);

	// 이렇게 작성할 경우, toggle 변수의 값이 변경될 때만 함수가 재생생됨
  // const onToggle = useCallback(() => setToggle(toggle), [toggle]);
  return (
    <div className="App">
      <button onClick={onClick} >Add 1</button>
      <button onClick={onToggle}>toggle</button>
      <div>{number}</div>
      <div>{toggle ? "true" : 'false'}</div>
    </div>
  );
}

export default App;

App은 state가 하나만 바뀌어도 onClick 만 바뀌어도 toggle도 또 같이 렌더링되기 때문에 이걸 막기위해 useCallback을 쓴다.

 

React.memo 는 props가 변경될때 컴포넌트를 기억. 

 

함수 생성과 함수 호출은 다르다. useCallback은 함수 생성만 한번. 호출과 관련된건 useMemo를 써야 방지가된다.