React 공식 문서 읽기 (2) - Basic Hooks

Chani·2022년 6월 1일
1
post-thumbnail

1. React Hook

리액트 훅은 리액트 ver16.8에서 소개되었다.

이전 클래스형 컴포넌트에서 사용할수 있었던 생명주기 메소드, 상태에 관한기능을 함수형 컴포넌트에서도 사용할수 있도록 지원해주기 위해 나온 친구이다.

그렇기 때문에 Hook은 기능별로 여러가지가 있고 여러가지 Hook에 대해 예제와 함께 설명해보려고 한다.

2. State Hook

1. useState

버튼을 누르면 숫자가 1씩 증가하는 간단한 컴포넌트를 생각해보자.

숫자가 바뀔때마다 바뀐 엘리먼트가 다시 렌더링 되면서 바뀐 숫자가 업데이트 되어야 하는데 함수 컴포넌트에서는 이걸 어떻게 해야할까.

답은 useState 에 있다. useState는 상태가 바뀌면 자동으로 컴포넌트의 바뀐 부분의 엘리먼트를 리렌더링 해준다.

아래 예제 코드를 참고해보자

const App = (): JSX.Element => {
  const [num, setNum] = useState<number>(0);

  return (
    <div>
      <p>{num}</p>
      <button onClick={()=>setNum(num + 1)}>숫자 늘리기</button>
    </div>
  )
}

useState는 위 코드와 같이 상태와 이 상태를 업데이트 해주는 함수를 쌍으로 제공한다.

처음으로 초기화 해주는 값은 useState 의 인자값으로 들어오게 되는데 타입스크립트이기떄문에 이 초기화 해주는 값이 어떤 타입인지 정의해줘야 한다.

위 예제에서는 number로 정의해주었다.

2. multiple state

그렇다면 상태가 여러개인 경우는 어떻게 처리를 해야할까?

그냥 useState를 여러개 선언해주면 된다.

import React, { useState } from 'react';

const App = (): JSX.Element => {
  const [num, setNum] = useState<number>(0);
  const [num1, setNum1] = useState<number>(1);

  const addNum = (): void => {
    setNum(num + 1);
    setNum1(num1 + 1);
  };

  return (
    <div>
      <p>{num}</p>
      <p>{num1}</p>
      <button onClick={addNum}>숫자 늘리기</button>
    </div>
  );
}

export default App;

위 예제 코드에서는 버튼을 누르면 증가하는 숫자가 두개가 되었다.

num과 num1의 숫자를 각각 1씩 증가하게 되는데 위쪽에서는 상태가 바뀌면 자동적으로 컴포넌트 바뀐 부분이 재렌더링 된다고 하였다.

그렇다면 num과 num1의 상태가 한번씩 총 두번 바뀌었으니 렌더링도 두번이 일어날까?

const [num, setNum] = useState<number>(0);
  const [num1, setNum1] = useState<number>(1);

  console.log('render');

  const addNum = (): void => {
    setNum(num + 1);
    setNum1(num1 + 1);
  };

이전 예제 코드의 useState와 addNum 함수 사이에 console.log를 넣어본 후 실행을 시켜보았다.

버튼을 한번 클릭했지만 콘솔에 찍히는 ‘render’는 맨 처음 렌더링 때와 버튼 클릭후 생긴 ‘render’로 2번만 console.log가 찍히는 것을 확인 할 수 있었다.

상태가 두번이 바뀌었지만 한번에 바뀌었기 때문에 리액트 내부에서 자동적으로 묶어서 한번만 재렌더를 해주는 것이다.

3. useState functional update

다음은 useState의 함수형 업데이트에 대해 알아보자.

import React, { useState } from 'react';

const App = (): JSX.Element => {
  const [num, setNum] = useState<number>(0);

  const addNum = (): void => {
    setNum(num + 1);
    setNum(num + 1);
  };

  return (
    <div>
      <p>{num}</p>
      <button onClick={addNum}>숫자 늘리기</button>
    </div>
  );
}

export default App;

다음 코드를 실행시킨후 버튼을 클릭하면 어떤 값이 나오게 될까?

리액트는 리렌더링 이후에야 비로소 state 상태가 반영되고, 상태를 동시에 업데이트 하는 경우 모든 업데이트를 묶어서 한번에 처리하기 때문에 (batching) 위 그림과 같이 2가 아니라 1로 업데이트 된다.

그렇다면 동기적으로 상태를 변경해주고 싶다면 어떻게 해야할까?

setState에 값을 그대로 전달하는게 아니라 함수를 전달하면 된다.

import React, { useState } from 'react';

const App = (): JSX.Element => {
  const [num, setNum] = useState<number>(0);

  const addNum = (): void => {
    setNum(num + 1);
    setNum((n) => n + 1);
  };

  return (
    <div>
      <p>{num}</p>
      <button onClick={addNum}>숫자 늘리기</button>
    </div>
  );
}

export default App;

위 코드와 같이 (n) ⇒ N + 1 를 setNum함수에 넘겨주었더니 아래와 같이 0에서 2로 바뀌었다.

setNum(num + 1) 에서 1로 바뀐 num을 받아서 1을 더해주었기 때문에 아래와 같은 결과가 발생한 것이다.

const addNum = (): void => {
    setNum((n) => n + 1);
    setNum(num + 1);
  };

그렇다면 setNum 함수의 위치를 바꿔주면 결과는 어떻게 될까?

setNum((n) ⇒ n + 1); 에서는 동적으로 num값을 받아 1로 바꿔주었지만 setNum(num + 1);에서 동기적으로 num값을 받아 처리해주는게 아니기 때문에 1로 고정이되어 1 → 1 이 되어서 바뀌지 않은것 처럼 보이는 것이다.

3. Effect Hook

1. useEffect

side effect란 무엇일까? 함수 내부에 있는 값이 아닌 외부에 있는 값을 참조하거나 수정하여 수행하는 것을 의미한다.

리액트로 구현하다보면 API를 통해 정보를 받아서 해당 정보를 참조하여 엘리먼트가 만들어지는 경우가 자주 생기는데 API를 통해 받은 정보는 외부의 값이기 때문에 이러한 경우 side effect가 발생한다고 할 수 있다.

리액트에서는 이러한 side effect를 관리해주기 위해 useEffect라는 Hook을 이용하게 된다.

const App = (): JSX.Element => {
  const [str, setStr] = useState<string>('');

  const func = async () => {
    const b = await fetch('https://httpbin.org/get');
    const c = await b.json();
    return c;
  }

  useEffect(() => {
    func().then((r) => {
      const a = JSON.stringify(r);
      setStr(a);
    });
  });

  return (
    <div>
      <p>{str}</p>
    </div>
  );
};

위 코드에서 func 함수는 API fetch 요청을 보내고 해당 값을 json 형태로 바꿔준 후 리턴해주게 된다.

이후 useEffect에서 str의 값을 func함수에서 받은 json을 string 형으로 바꿔준 값으로 변경해주게 되는 코드이다.

현재 컴포넌트에서 API로 받은 값을 참조하고 있으므로 side effect가 발생하고 있다고 할 수 있고, 그렇기 때문에 해당부분을 useEffect에 넣어 관리해주고 있는것이다.

2. why useEffect

const func = async () => {
    const b = await fetch('https://httpbin.org/get');
    const c = await b.json();
    return c;
  }

  func().then((r) => {
    const a = JSON.stringify(r);
    setStr(a);
  });

  return (
    <div>
      <p>{str}</p>
    </div>
  );

위 코드처럼 useEffect를 사용하지 않더라도 처음의 코드와 실행 결과는 같지만 내부적으로 코드가 실행되는 순서는 다르다.

useEffect 내부 함수가 실행되는 시기는 리액트의 DOM이 렌더링 되고 난 이후이므로 useEffect를 사용하면 함수 자체가 렌더링 이후에 실행되게 되는 반면, 위 코드처럼 useEffect를 사용하지 않으면 함수가 선언된 이후에 바로 실행되게 된다.

실행 결과는 같기 때문에 위 코드들 처럼 매번 리렌더링 될때마다 useEffect를 실행되도록 코드를 작성한다면 useEffect를 사용하지 않아도 된다고 생각할수 있지만 그렇게 코드를 짜는것은 바람직하지 못하다.

useEffect(() => {
    setNum(num + 1);
  });

리액트는 주어진 렌더에서 어떤 데이터가 어떤 Hooks 호출과 연관되어 있는지 알기 위해 Hooks 호출의 순서를 매핑하는데 이러한 과정을 통해 위 코드와 같은 무한으로 재렌더링 되는 오류를 검출한다.
(Hooks의 규칙과 순서에 관한 이야기는 다음 포스팅에서 좀 더 자세히 다룰 예정이다.)

즉, side effect를 발생시키는 코드를 useEffect에 넣어서 관리해주지 않는다면 코드에서 오류가 발생했을때 적절히 추적하는게 어렵게 될 가능성이 높다.

따라서 side effect를 발생시키는 코드는 그 역할에 맞게 useEffect Hook 내부에서 관리해주는것이 바람직 할 것이다.

3. useEffect dependency array

그렇다면 useEffect는 항상 재렌더링 될 때마다 실행이 매번 되는것일까?

const App = (): JSX.Element => {
  const [str, setStr] = useState<string>('');
  const [num, setNum] = useState<number>(0);

  const func = async () => {
    const b = await fetch('https://httpbin.org/get');
    const c = await b.json();
    return c;
  }

  useEffect(() => {
    func().then((r) => {
      const a = JSON.stringify(r);
      setStr(a);
    });
  }, [num]);

  return (
    <div>
      <p>{str}</p>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>숫자 늘리기</button>
    </div>
  );
};

위 코드의 예시를 보자.

useEffect의 두번째 인자에 아무것도 들어가지 않는경우 재렌더링 될 때마다 useEffect가 실행이 되지만 위 코드처럼 의존성 배열에 값을 넣어주면 해당 값이 바뀔때만 useEffect가 실행이 된다.

즉, 위 코드에서는 num이 바뀌는 경우만 새롭게 API를 요청받아 값이 str값이 바뀌게 된다.

4. useEffect clean-up

리액트에서 구현을 할때면 구독을 추가하거나 제거하는 로직이 들어가야하는 경우가 발생하기도 한다. 이러한 경우에는 useEffect의 clean-up을 사용하면 쉽게 해결이 가능하다.

const App = (): JSX.Element => {
  const [num, setNum] = useState<number>(0);

  const a = () => {
    setNum((n) => n + 1);
  }

  useEffect(() => {
    const strDiv = document.getElementById('strDiv');
    console.log(num);
    strDiv?.addEventListener('click', a);
  });

  return (
    <div>
      <div id='strDiv'>{"str"}</div>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>숫자 늘리기</button>
    </div>
  );

위 코드의 경우 useEffect가 실행되면 strDiv를 클릭시 num이 1씩 증가하는 이벤트를 달아주었다.

이제 버튼뿐 아니라 strDiv를 클릭해도 숫자가 1씩 늘어날 것이다.

하지만 예상과는 다르게 4번을 클릭했는데 숫자가 15까지 늘어났다.

왜 이런 현상이 발생하게 되는것일까

이유는 strDiv에 클릭이벤트가 중첩되어 들어갔기 때문이다.

useEffect가 실행될때마다 setNum((n) ⇒ n + 1); 이벤트가 추가가 되기만하고 해제되지 않기때문에 클릭할때마다 중첩된 이벤트 모두가 실행되어 이런 결과가 발생하게 되는것이다.

const strDiv = document.getElementById('strDiv');
    console.log(num);
    strDiv?.addEventListener('click', a);
    return () => {
      strDiv?.removeEventListener('click',a);
    }
  });

useEffect 코드의 return 에 다음과 같이 추가해보자.

useEffect의 return에 들어가는 함수는 해당 컴포넌트가 언마운트 될때 실행되게 된다.

즉, 처음 컴포넌트가 마운트 될때는 클릭이벤트가 등록이 되고, 상태가 변경되어 재렌더링되면 재렌더링 되기전에 return안에 있는 함수가 실행이 된다.

이렇게 하면 원래 의도했던대로 4번을 클릭한 경우 숫자가 4까지만 늘어나는것을 확인할 수 있다.

profile
프론트엔드에 스며드는 중 🌊

1개의 댓글

comment-user-thumbnail
2022년 6월 2일

잘읽었습니다.

답글 달기