React Hooks

성석민·2022년 1월 11일
1

프론트엔드

목록 보기
2/7
post-thumbnail

useState

컴포넌트가 보여줘야할 내용이 사용자의 상호작용에 따라 동적으로 값이 바뀌어야 할 때 사용할 수 있는 React Hooks 중 하나이다.

useState는 배열을 리턴하는데 첫 번째 인자는 state의 초깃값이고, 두 번째 인자는 값을 갱신할 수 있는 함수이다.

useState

const [state, setState] = useState();

그렇다면 어떻게 값을 동적으로 변경하고 사용자에게 보여지는지 코드를 통해 확인해보자.

import React, { useState } from 'react';

const App = () => {
  // 1. useState를 통해 counter 변수의 초기값을 0으로 설정
  const [counter, setCounter] = useState(0);

  // 3. setCounter 함수를 통해 counter의 값을 재할당
  const increaseCounterHandler = () => {
    setCounter(counter + 1);
  };

  return (
    <div>
      {counter}
      // 2. 버튼이 클랙되면 increaseCounterHandler 이벤트 호출
      <button onClick={increaseCounterHandler}>+</button>
    </div>
  );
};

export default App;

리액트는 컴포넌트의 state나 props의 값이 변경되면 리렌더링 과정을 거치게 된다.

3번의 setCounter함수가 호출된 후 counter(즉, state)의 값이 변경되면 리렌디링이 되고 counter는 1이 증가한 값(즉, 2)이 되므로 화면에 2가 출력된 모습을 볼 수 있다.

하지만 이러한 useState의 두 번째 인자(setState, 해당 글에서는 setCounter)는 비동기적으로 처리된다는 특징을 가지고 있다.

...
const [counter, setCounter] = useState(0);

const increaseCounterHandler = () => {
    setCounter(counter + 1); // 리렌더링이 일어나지 않는다.
    setCounter(counter + 1); // 리렌더링이 일어나지 않는다.
    setCounter(counter + 1); // 리렌더링이 일어나지 않는다.
  };
// increaseCounterHandler 함수가 종료되면 리렌더링 된다.

...

위와 같이 counter를 총 3번 업데이트시키는 함수라고 가정했을 때 우리가 기대하는 값은 3이 증가한 값이겠지만 실질적으로는 1밖에 증가하지 않는다.

리액트에서 이러한 현상을 batching이라고 하고 더 나은 성능을 위해 여러 개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미한다. 이러한 리액트의 특징 때문에 동일한 state를 연속적으로 업데이트하는 경우 setState를 동기적으로 수행하지 않고 일괄적으로 처리한다.

그렇다면 위의 코드에서 어떻게 하면 3씩 증가한 값을 기대할 수 있을까 ?

방법은 이전 state의 값을 받아 1씩 더한 값을 리턴해주면 된다.

...
const [counter, setCounter] = useState(0);

const increaseCounterHandler = () => {
    setCounter(prevState => prevState + 1); // 반환값 : 1
    setCounter(prevState => prevState + 1); // 반환값 : 2
    setCounter(prevState => prevState + 1); // 반환값 : 3
  };

...

함수는 호출되는 시점에서 CallBack Queue에서 차례대로 실행된다. 그렇기 때문에 setCounter의 prevState는 값이 변경된 최신의 값을 유지할 수 있다.

useEffect

컴포넌트가 화면에 Mount : 화면에 첫 렌더링, Update: 다시 렌더링, Unmount : 화면에서 사라질 때 특정 작업을 처리할 수 있게 도와주는 React Hooks 중 하나이다.

useEffect는 (effect, deps?)의 구조를 갖는다.

useEffect

  • effect : 수행할 작업 ex) API 호출, Timer 설정, 구독 설정
  • deps : 배열 형태를 가지고 있으며 변화를 감지하고 싶은 값을 지정

Always

컴포넌트가 렌더링 될 때마다 실행된다.

useEffect(() => {
  // 작업...
})

Mount

화면에 첫 렌더링 될 때 딱 한 번만 호출된다.

useEffect(() => {
  // 작업...
}, [])

Update

dependency 배열에 포함된 특정 값(state or props)가 변경되면 호출된다.

useEffect(() => {
  // 작업...
}, [value])

Unmount

컴포넌트가 화면에서 사라질 때 호출된다.

// clean up function 이라고 불린다.
useEffect(() => {
  // 작업...

  return () => {
    // 작업 해지 ex) timer, addEventListner ...
  }
}, [value])

useCallback과 useMemo를 알기 전 알야아 하는 React 특징

  • 컴포넌트가 렌더링 될때 마다 내부에 선언되어 있던 변수, 함수등도 매번 다시 선언된다.
  • 컴포넌트는 자신의 state가 변경되거나 또는 부모에게서 받은 props의 값이 변경될 때마다 리렌더링 된다.

useCallback

메모리제이션된 함수를 반환하는 컴포넌트의 최적화를 위한 React Hooks중 하나이다.

useCallback(() => {
	// 작업
}, [종속성 배열]);

종속성 배열은 useEffect 두 번째 인자와 같은 역할을 한다.

리액트에서의 함수는 Reference Type으로 주소 값을 참조하고 있다.

const aFunc () => console.log('hi');

const bFunc () => console.log('hi');

console.log(aFunc === bFunc) // false

위 두 개의 함수는 같은 기능을 하지만 서로 참조하고 있는 주소 값이 다르므로 같은 함수라고 볼 수 없다.

리액트는 컴포넌트가 렌더링 될 때마다 내부에 선언되어 있던 변수, 함수가 다시 선언된다는 특징을 가지고 있다.

function App() {
	const [checked, setChecked] = useState(false);
  const [toggleBtn, setToggleBtn] = useState(false);

  const checkedHandler = () => setChecked((prevState) => !prevState);
  const showParagraphHandler = () => setToggleBtn((prevState) => !prevState);

  return (
    <div>
      {toggleBtn ? <p>보인다.</p> : <p>안 보인다.</p>}
      <button onClick={checkedHandler}>Checked Button</button>
      <button onClick={showParagraphHandler}>Toggle Button</button>
    </div>
  );
}

export default App;

Toggle Button을 클릭할 때마다 App 컴포넌트의 toggleBtn state는 false에서 true로, true에서 false로 변경될 것이고, 변경될 때마다 toggleBtn state가 바뀌기 때문에 App 컴포넌트는 리렌더링 되고 App 컴포넌트 안에 있는 showParagraphHandler 함수와 toggleBtn과 상관없는 checkedHandler 함수는 계속해서 선언될 것이다.

매번 리렌더링 될 때마다 재선언되는 checkedHandler와 showParagraphHandler함수는 각각 같은 주소를 참조하고 있을까 ?

결론적으로는 아니다. 그 이유는 함수의 내용이 같더라도 선언될 때마다 주소 값이 달라지기 때문이다.

해당 state에 직접적으로 관여되지 않는 함수가 굳이 실행되어야 할 이유가 있을까 ? 이것도 아니다.

그렇기에 해결책은 useCallback이다.

import { useCallback, useState } from 'react';

function App() {
	const [checked, setChecked] = useState(false);
  const [toggleBtn, setToggleBtn] = useState(false);

  const checkedHandler = useCallback(() => setChecked((prevState) => !prevState), [checked]);
  const showParagraphHandler = useCallback(() => setToggleBtn((prevState) => !prevState), [toggleBtn]);

  return (
    <div>
      {toggleBtn ? <p>보인다.</p> : <p>안 보인다.</p>}
      <button onClick={checkedHandler}>Checked Button</button>
      <button onClick={showParagraphHandler}>Toggle Button</button>
    </div>
  );
}

export default App;

Checked Button을 눌렀을 때 checked의 state가 변경되고 리렌더링이 일어나도 toggleBtn의 state는 변경되지 않았기 때문에 showParagraphHandler 함수는 재선언 되지 않는다.

반대로 Toggle Button을 눌렀을 때 toggleBtn의 state가 변경되고 리렌더링이 일어나도 checked의 state는 변경되지 않았기 때문에 checkedHandler 함수는 재선언 되지 않는다.

useMemo

메모리제이션된 값을 반환하는 컴포넌트의 최적화를 위한 React Hooks중 하나이다.

const App = () => {
  const [title, setTitle] = useState('My Title');

  const titleChangeHandler = (any) => setTitle(any);

  const lists = useMemo(() => [1, 5, 3, 7, 2], []);

  return (
    <div>
      <Lists title={title} lists={lists} onChangeTitle={titleChangeHandler} />
    </div>
  );
};

const Lists = ({ title, lists, onChangeTitle }) => {
  console.log('Lists Running');

  const listCount = lists.sort((a, b) => a - b);

  return (
    <div>
      {title}
      <ul>
        {listCount.map((list, index) => (
          <li key={index}>{list}</li>
        ))}
      </ul>
      <button onClick={() => onChangeTitle('Your Title')}>Title Change</button>
    </div>
  );
};

위 코드와 같이 App 컴포넌트에서 Lists 컴포넌트에 title, lists, onChangeTitle 함수를 props로 전달해준다고 가정해보자.

Lists 컴포넌트에서 Title Change 버튼을 클릭 시 title(즉, props)이 변경될 것이고 그에 따라 부모 컴포넌트가 리렌더링 되면서 Lists 컴포넌트도 리렌더링 될 것이다.

하지만 lists(즉, 리스트 배열)는 변경이 없음에도 불구하고 lists를 정렬하는 listCount로 인해서 계속해서 계산 할 것이다. 만약 이러한 계산이 엄청 복잡한 로직이라면 분명히 성능이 좋아지지 않을 것이다.

이를 해결하기 위해 이전에 계산한 값을 기억했다가 재사용할 수 있는 함수가 useMemo이다.

...

const listCount = useMemo(() => {
    return lists.sort((a, b) => a - b);
  }, [lists]);

...

이제 listCounts는 Lists 컴포넌트가 리렌더링이 된다고 하더라도 lists의 변화가 없을 시에는 계산을 다시 하지 않고 이전의 값을 재사용 할 수 있게 되었다.

틀린 부분이 있거나 보충해야 할 내용이 있다면 댓글이나 DM(sungstonemin)으로 알려주시면 감사하겠습니다😄

profile
기록하는 개발자

0개의 댓글