[React] Hook

sikkzz·2023년 8월 16일
0

React

목록 보기
5/12
post-thumbnail

🖐️ 시작하며

React 에서 현재 당연시하게 쓰이고 있고 본인 또한 개발할 때 많이 사용하는 useState, useEffect 등 다양한 react hook들이 존재합니다. 어떻게 작동하고 써야하는지 다시한번 정확히 짚어보기 위해 이 글을 작성했습니다.

Hook의 개요

Hook은 React 버전 16.8부터 React 요소로 새로 추가되었습니다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있습니다.

위 글은 React 공식 문서 홈페이지에 적혀있는 내용입니다. Hook은 React v16.8부터 새로 도입된 기능으로 이를 활용하여 클래스 컴포넌트를 작성할 필요 없이 함수형 컴포넌트에서도 state 관리와 생명 주기 메소드 등 다양한 React 기능들을 사용할 수 있습니다.

등장 과정

React 공식 문서에서 얘기하는 Hook의 개발 이유로 3가지를 언급합니다.

  1. 컴포넌트 사이에서 상태 로직을 재사용하기 어렵습니다.
  2. 복잡한 컴포넌트들은 이해하기 어렵습니다.
  3. Class는 사람과 기계를 혼동시킵니다.

공식 문서에 나와있는 내용들을 정리해보면 render propsHOC(고차 컴포넌트)의 패턴을 통해 재사용 가능한 상태 로직을 해결하는 부분에 대한 문제점, 각 생명주기 메서드에 존재하는 관련 없는 로직들이 많음으로써 발생하는 컴포넌트 코드가 복잡해진다는 문제점, 클래스형 컴포넌트에서는 constructor, this, binding 등 많은 코드의 규칙으로 인해 코드의 최소화가 힘들다는점 등이 있었습니다.

  • render props란 react 컴포넌트 간에 코드를 공유하기 위해 함수 props를 이용하는 간단한 테크닉입니다.
  • HOC(고차 컴포넌트)는 컴포넌트 로직을 재사용하기 위한 React의 기술로 간단히 말해, 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.

결론적으로는 클래스형과 함수형 두 가지의 컴포넌트 중 상태 관리 및 생명 주기 메서드 이용을 위해 클래스형 컴포넌트를 사용하기에는 너무나도 많은 문제점이 존재했고 코드의 간소화를 위해 함수형 컴포넌트에서도 상태 관리, 생명 주기 메서드 이용을 위해 Hook을 개발한 것이라고 합니다.

다만 공식 문서에서는 클래스형 컴포넌트를 제거할 계획은 없기에 이미 작성된 코드에 대해서는 "리팩토링"의 필요성을 느끼지는 못한다고 합니다.

Hook의 규칙

Hook은 JavaScript 함수입니다. 하지만 Hook을 사용할 때는 두 가지 규칙을 준수해야 합니다.

1) 최상위에서만 Hook을 호출해야 합니다

반복문, 조건문 혹은 중첩된 함수 내에서는 Hook을 호출하면 안됩니다. 항상 React 함수의 최상위에서 Hook을 호출해야 합니다.

import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  
  return (
    ...
  );
}

export default Counter;

React는 Hook의 호출 순서에 의존하기에 React가 상태값을 구분할 수 있는 유일한 정보는 Hook의 순서입니다.

import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  
  if (count < 5) {
    useEffect(function addCount() {
      setCount(count + 1);
    }, [count]);
  }
}

위 코드처럼 반복문이나 조건문 안에서 Hook을 사용할 경우 상황에 따라 조건이 변경되면서 Hook의 호출을 건너뛰고 실행순서가 바뀜으로써 오류가 발생할 수 있는 것입니다.

import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(function addCount() {
    if (count < 5) {
      setCount(count + 1);
    }
  },[count]);
}

반복문이나 조건문을 사용하고 싶다면 위 코드처럼 useEffect안에 넣어서 사용해야 합니다.

2) 오직 React 함수 내에서 Hook을 호출해야 합니다

Hook을 React 함수가 아닌 JavaScript 함수에서 호출하면 안됩니다. React 함수 컴포넌트나 Custom Hook에서 Hook을 호출해야 합니다.

Hook의 종류

1) useState

useState는 가장 기본적인 Hook으로 사용자가 직접 업데이트할 수 있는 state 변수를 선언하고 관리할 수 있습니다.

useState의 기본 문법은 다음과 같습니다.

const [state, setState] = useState(initialState)

useState는 처음 렌더링할 때 초기 상태 값(initialState)을 인수로 전달 받고, 최신 state의 값을 유지하는 변수와 그 값을 업데이트할 수 있는 함수를 반환합니다.

다음 코드는 숫자 카운터를 useState를 사용하여 구현한 코드입니다.

import React, { 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>
  );
};

value의 초기값은 0으로 초기화되어있습니다. 버튼을 클릭시 useState로 선언했던 value의 값을 변경할 수 있습니다.

useState 함수의 파라미터에는 상태 초기값을 넣어줍니다. 함수가 호출되면 배열을 반환하는데 배열의 첫 번째 원소는 상태 값(위에서는 value)이고 두 번째 원소는 상태를 변경하는 함수(위에서는 setValue)입니다. 함수에 파라미터를 넣어서 호출하게 되면 전달받은 파라미터로 값이 변경되고 컴포넌트는 리렌더링 됩니다.

위에서 언급했던 클래스형과 함수형 컴포넌트의 상태 관리 코드를 작성해보겠습니다.코드를 확인해보시면 코드의 간결함이나 편리성을 눈에 띄게 확인하실 수 있을 것입니다.

// 클래스형 컴포넌트
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
}
// 함수형 컴포넌트
import React, { useState } from "react";

const Exmaple = () => {
  const [count, setCount] = useState(0);
}

2) useEffect

useEffect는 React 컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook입니다. 클래스형 컴포넌트의 componentDidMount와 componentDidUpdate를 합친 형태와 동일합니다.

useEffect의 기본 문법은 다음과 같습니다.

useEffect(setup, dependencies)

첫 번째 파라미터에는 함수(setup)가 들어갑니다. 이 함수는 컴포넌트가 렌더링된 이후에 호출됩니다. 두 번째 파라미터에는 의존값이 들어있는 배열(dependencies)이 들어갑니다. 또한 useEffect에서는 함수를 반환할 수 있는데 이 함수를 cleanup 함수라고 부릅니다. cleanup 함수는 useEffect에 대한 정리를 해준다고 보면 됩니다.

2.1 마운트 될 때만 실행

useEffect에서 설정한 함수가 컴포넌트가 화면에 가장 처음 렌더링 될 때(마운트 될 때)만 실행되고 업데이트를 할 필요가 없는 경우에는 함수의 두 번째 파라미터에 비어있는 배열을 넣으면 됩니다.

useEffect(() => {
  console.log('마운트 될 때만 실행');
}, []);

위 코드처럼 작성하면 컴포넌트 첫 렌더링때만 useEffect가 실행되어 console에 문구가 나타나고 그 이후에는 작동하지 않습니다.

2.2 특정 값이 업데이트 될 때만 실행

useEffect를 사용할 때 특정 값이 변경될 때 함수가 호출되게 할 수 있습니다.

useEffect(() => {
  console.log(name);
}, [name]);

위 코드처럼 작성하면 state로 지정해둔 name의 값이 변경될 때 마다 console에 문구가 나타나게 됩니다.

2.3 cleanup 함수 사용

만약 컴포넌트가 언마운트되기 전이나, 업데이트 되기 직전에 특정 작업을 실행하고 싶다면 useEffect에서 cleanup 함수를 반환해주면 됩니다.

useEffeect(() => {
  console.log('effect');
  return () => {
    console.log('cleanup');
  };
});

위 코드에서 return 구문이 cleanup 함수에 해당됩니다. 렌더링이 진행될 때마다 cleanup 함수가 계속 실행됩니다. 언마운트 될 때만 cleanup 함수를 호출하고 싶으시다면 useEffect 함수의 두 번째 파라미터에 빈 배열을 넣으면 됩니다.

3) useLayoutEffect

useLayoutEffectuseEffect와 비슷하지만 다른점이 있습니다.

useEffect vs useLayoutEffect

  1. useEffectasynchronous로 비동기적으로 실행되지만 useLayoutEffectsynchronous로 동기적으로 실행됩니다.
  2. render 순서에 차이가 있습니다.

두 Hook의 render 순서를 보기전에 두 가지 개념부터 알아보겠습니다.

  • Render: DOM Tree를 구성하기 위해 각 엘리먼트의 스타일 속성을 계산하는 과정
  • Paint: 실제 스크린에 Layout을 표시하고 업데이트하는 과정

useEffect는 컴포넌트들이 render와 paint된 후 실행됩니다. paint된 후 실행되기 때문에 useEffect 코드로 인해 DOM에 영향을 주는 코드가 있는 경우 사용자는 리렌더링으로 인한 화면 새로고침(깜빡임)을 경험하게 됩니다. 아래 그림은 useEffect의 라이프사이클 그림입니다.

useLayoutEffect는 컴포넌트들이 render된 후 실행되고 paint가 실행됩니다. paint가 되기 전에 실행되기 때문에 DOM에 영향이 있는 코드가 useLayoutEffect에 존재하더라도 사용자는 리런데링으로 인한 화면 새로고침(깜빡임)을 경험하지 않습니다. 아래 그림은 useLayoutEffect의 라이프사이클 그림입니다.

결론적으로 두 Hook의 차이는 동기, 비동기적 실행과 Hook 실행 시점에서 차이가 있습니다.

useLayoutEffect는 내부의 코드가 모두 실행된 후 paint 작업이 실행되기 때문에 로직이 복잡할 경우 사용자가 레이아웃을 보는데까지 걸리는 시간이 비교적 길어질 수 있습니다. 보편적으로는 useEffect를 사용하되 특정 상황에서 필요한 경우에만 useLayoutEffect를 사용하는걸 권장합니다.

import React, { useLayoutEffect, useState, useEffect } from 'react'

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

  useEffect(() => {
    if(value === 0){
      setValue(10 + Math.random() * 200)
    }
  }, [value])

  return (
    <button onClick={() => setValue(0)}>
      value: {value}
    </button>
  )
}

export default App
import React, { useLayoutEffect, useState } from 'react'

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

  useLayoutEffect(() => {
    if(value === 0){
      setValue(10 + Math.random() * 200)
    }
  }, [value])

  return (
    <button onClick={() => setValue(0)}>
      value: {value}
    </button>
  )
}

export default App

위 두 예제는 같은 예제를 useEffect, useLayoutEffect로 각각 구성한 것입니다. 실행시켜보면 useEffect 예제는 버튼 값이 잠깐 사라지는 걸 확인할 수 있습니다. 그에비해 useLayoutEffect 예제는 버튼 값이 사라지는 부분 없이 부드럽게 작동하는 걸 확인할 수 있습니다.

4) useContext

useContext는 함수형 컴포넌트에서 Context를 보다 더 쉽게 사용할 수 있게 해줍니다. 우선 Context에 대해 알아보겠습니다.

Context란?

React 공식 문서에 나와있기를 context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다고 나와있습니다.

만약 props를 통해 전달되는 컴포넌트의 갯수가 10개, 20개, 100개 등 많아지게 되면 props의 추적이 어려워지고 코드의 유지보수 또한 어려워집니다. context를 이용하면 컴포넌트마다 명시적으로 props를 넘겨주지 않아도 컴포넌트들이 데이터 공유가 가능해집니다.

다음 코드는 Context를 이용한 예제입니다.

// App.js
import React, { createContext } from "react";

import Children from "./Childrend";

export const ContextTest = createContext();

const App = () => {
  const user = {
    name: "Kim",
    age: "20"
  };
  
  return (
    <ContextTest.Provider value={user}>
      <div>
        <Children />
      </div>
    </ContextTest.Provider>
  );
};

export default App;
// Children.js
import React from "react";
import { ContextTest } from "./App";

const Children = () => {
  return (
    <ContextTest.Consumer>
      {(user) => (
        <>
          <h1>ContextTest name값 : {user.name}</h1>
          <h1>ContextTest age값 : {user.age}</h1>
        </>
      )}
    </ContextTest.Consumer>
  );
};

export default Children;

위 예제에서는 하나의 컴포넌트에서만 Context를 사용했지만 컴포넌트가 늘어나고 공유해야하는 데이터값이 늘어난다면 코드가 복잡해지고 반복되는 코드가 늘어날 것입니다.

useContext 사용

useContext는 context 객체를 인수로 전달 받아 해당 context의 현재 값을 반환하며, useContext를 호출한 컴포넌트는 해당 context의 값이 변경될 때마다 리렌더링 됩니다.

기본 문법은 다음과 같습니다.

const value = useContext(Context)

위 예제를 useContext를 사용하여 변경해보겠습니다. App.js의 코드는 동일합니다.

// Children.js
import React, { useContext } from "react";
import { ContextTest } from "./App";

const Children = () => {
  const user = useConext(ContextTest);
  return (
    <>
      <h1>ContextTest name값 : {user.name}</h1>
      <h1>ContextTest age값 : {user.age}</h1>
    </>
  );
};

export default Children;

App.js에서 Context를 넘겨주는 코드는 동일하지만 Children.js에서 useContext를 사용하여 Context의 데이터를 바로 이용할 수 있습니다. 하나의 컴포넌트에서는 명확히 줄어드는 코드양이 적을지는 몰라도 이게 여러 컴포넌트가 반복되면 코드를 간소화 시키기 편하다는 장점이 있습니다.

다만 React에서 context의 사용은 컴포넌트의 재사용이 어렵게 만들기 때문에 사용을 권장하지는 않습니다. 상태 관리는 Redux와 같은 전역 상태 관리 라이브러리를 사용해 context를 사용하는 것을 추천합니다.

5) useReducer

useReducer는 state를 관리하고 업데이트하는 Hook인 useState와 비슷합니다. state를 관리하고 업데이트 하지만 컴포넌트 내부가 아닌 컴포넌트 외부에서 state 업데이트 로직을 구성하므로 컴포넌트로부터 상태 관리 로직을 분리시킬 수 있습니다.

useReducer의 핵심 요소는 3가지가 있습니다.

  • reducer
  • dispatch
  • action

기본 문법은 다음과 같습니다.

import React, { useReducer } from "react";

const reducer = (state, action) => {};

const initialState = {};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    ...
  );
}

export default App;

reducer 함수를 통해 state와 action을 인자로 받아서 dispatch 함수를 통해 state를 변경시켜줍니다.

dispatch 함수를 사용하여 state를 업데이트 할 수 있습니다.

initialState: state의 초기값을 담을 변수입니다.

아래 두 가지 예제를 살펴보겠습니다. 하나는 useState를 이용한 Counter 예제이고 다른 하나는 useReducer를 이용한 Counter 예제입니다.

import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  
  const onDecrease = () => {
    setCount((prevCount) => prevCount - 1);
  };
  
  const onIncrease = () => {
    setCount((prevCount) => prevCount + 1);
  };
  
  return (
    <>
      <h1>Count: {number}</h1>
      <button onClick={onDecrease}>-</button>
      <button onClick={onIncrease}>+</button>
    </>
  );
}

export default Counter;
import React, { useReducer } from "react";

const reducer = (state, action) => {
  switch(action.type){
    case 'decrement':
  	  return state - 1;
    case 'increment':
  	  return state + 1;
    default:
  	  throw new Error();
  }
}

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

export default Counter;

reducer 함수에는 state와 state를 업데이트할 case들이 들어가게 됩니다. 위 예제에서는 count가 state에 해당됩니다. 이 case들이 action에 해당되며 위 예제에서는 'decrement'와 'increment'가 각각의 action에 해당됩니다.

Counter 함수로 내려와서 useReducer를 선언하고 위에서 작성한 reducer와 initialState 값으로 0을 지정해주었습니다. 그 후 button이 클릭되면 dispatch 함수를 통해 각각의 action을 실행시키면 reducer 함수에 작성된 state 업데이트 기능들이 실행됩니다.

아마 Redux를 경험해봤다면 해당 코드가 익숙하게 느껴질 것입니다. 두 예제의 가장 큰 차이점은 count의 기능이 reducer 함수로 분리되어 있다는 것입니다. 해당 예제에서는 같은 파일 안에 다른 함수로 작성했지만 reducer 함수를 파일로 따로 만들어서 export, import를 통해 다른 컴포넌트에서도 같은 로직을 사용할 수 있다는 장점이 있습니다.

6) useMemo

useMemo 는 React에서 컴포넌트의 성능 최적화를 위해 도입된 Hook입니다. memoization된 값을 반환하며 동일한 계산을 반복해야 할 때, 또는 특정 값이 변경되었을 때만 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행속도를 빠르게 할 수 있습니다.

기본 문법은 다음과 같습니다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

아래 예제를 먼저 살펴보겠습니다.

// App.js
import React, { useState } from "react";
import Show from "./Show";

const App = () => {
  const [number, setNumber] = useState(0);
  const [text, setText] = useState("");

  const onIncrease = () => {
    setNumber((prevNum) => prevNum + 1);
  };

  const onDecrease = () => {
    setNumber((prevNum) => prevNum - 1);
  };

  const onChangeText = (e) => {
    setText(e.target.value);
  };

  return (
    <div>
      <button onClick={onIncrease}>+</button>
      <button onClick={onDecrease}>-</button>
      <input type="text" placeholder="text 입력" onChange={onChangeText} />
      <Show number={number} text={text} />
    </div>
  );
};

export default App;
// Show.js
import React from "react";

const Number = (num) => {
  console.log("숫자 변경");
  return num;
};

const Text = (text) => {
  console.log("글자 변경");
  return text;
};

const Show = ({ number, text }) => {
  const showNumber = Number(number);
  const showText = Text(text);

  return (
    <div>
      {showNumber}
      <br />
      {showText}
    </div>
  );
};

export default Show;

위 예제를 실행시키고 숫자 값이나 텍스트 값을 변경하면 둘 중 하나만 변경하는데도 숫자 변경과 글자 변경 둘 다 console창에서 확인하실 수 있습니다.

숫자만 변경했는데도 글자 변경이 console창에 뜬다는 것은 Number와 Text 함수가 둘 다 실행된다는건 비효율적이고 메모리 낭비라는걸 다들 알 수 있을 것입니다.

이 예제에 useMemo 를 사용해보겠습니다. App.js 코드는 동일합니다.

// Show.js
import React, { useMemo } from "react";

const Number = (num) => {
  console.log("숫자 변경");
  return num;
};

const Text = (text) => {
  console.log("글자 변경");
  return text;
};

const Show = ({ number, text }) => {
  const showNumber = useMemo(() => Number(number), [number]);
  const showText = useMemo(() => Text(text), [text]);

  return (
    <div>
      {showNumber}
      <br />
      {showText}
    </div>
  );
};

export default Show;

아까처럼 예제를 실행시켜보면 이제 변동되는 값에 대해서만 console창을 통해 함수 작동을 확인하실 수 있습니다.

당연히 이런 간단한 예제로는 성능에 큰 영향이 생기지는 않습니다. 간단한 계산들이기 때문입니다.

기본 문법에서 함수 네이밍을 보면 computeExpensiveValue인 것을 확인하실 수 있습니다. 해당 기본 문법은 React 공식 홈페이지에 나와있는 문법입니다. 함수 이름이 computeExpensiveValue인 이유는 다음과 같습니다.

memoization되는 값의 재계산 로직이 expensive한 경우, 즉 복잡한 계산일 경우에 useMemo를 사용하는 것을 권장합니다. 복잡한 계산의 경우만 useMemo는 성능상 큰 이점으로 작용합니다.

그렇기에 간단한 프로그램에서는 useMemo의 이점을 보지 못한다는 것입니다. 그렇기에 복잡한 로직의 경우에만 useMemo를 사용하는 것을 권장합니다.

7) useCallback

useCallbackuseMemo와 마찬가지로 성능 최적화를 위해 도입된 Hook이며 memoization된 콜백을 반환합니다. 특정 함수를 새로 만들지 않고 재사용할 때 주로 사용합니다.

기본 문법은 다음과 같습니다.

const memoizedCallback = useCallback(
  () => { 
  	doSomething(a, b);
  },
  [a, b],
);

첫 번째 파라미터에는 생성하고 싶은 함수를, 두 번째 파라미터에는 특정 값을 넣게 되는데 특정 값이 변할 때 첫 번째 파라미터의 함수가 생성되는 구조입니다.

아래 예제를 먼저 살펴보겠습니다.

// App.js
import React, { useState } from "react";
import Show from "./Show";

const App = () => {
  const [number, setNumber] = useState(0);
  const [text, setText] = useState("");

  const getNum = () => {
    return number
  }

  const onIncrease = () => {
    setNumber((prevNum) => prevNum + 1);
  };

  const onDecrease = () => {
    setNumber((prevNum) => prevNum - 1);
  };

  const onChangeText = (e) => {
    setText(e.target.value);
  };

  return (
    <div>
      <button onClick={onIncrease}>+</button>
      <button onClick={onDecrease}>-</button>
      <input type="text" placeholder="text 입력" onChange={onChangeText} />
      <Show getNum={getNum} text={text} />
    </div>
  );
};

export default App;
// Show.js
import React, { useEffect, useState } from "react";

const Show = ({ getNum, text }) => {
  const [num, setNum] = useState();

  useEffect(() => {
    setNum(getNum());
    console.log("숫자 변동");
  }, [getNum]);

  return (
    <div>
      {num}
      <br />
      {text}
    </div>
  );
};

export default Show;

예제를 실행시키고 버튼을 클릭하여 숫자를 변동시키면 console창에 "숫자 변동"이 찍히는걸 볼 수 있습니다. 다만 Input창에 텍스트를 입력해도 console창에 동일하게 "숫자 변동"이 찍히게 됩니다.

텍스트를 변경할때는 Show.js의 useEffect가 실행되지 않아야 하기에 "숫자 변동"이 찍히면 안됩니다. useCallback을 사용하여 해당 문제점을 해결할 수 있습니다. 코드를 다음과 같이 변경해보겠습니다. Show.js의 코드는 동일합니다.

// App.js
import React, { useState, useCallback } from "react";
import Show from "./Show";

const App = () => {
  const [number, setNumber] = useState(0);
  const [text, setText] = useState("");

  const getNum = useCallback(() => {
    return number;
  }, [number]);

  const onIncrease = () => {
    setNumber((prevNum) => prevNum + 1);
  };

  const onDecrease = () => {
    setNumber((prevNum) => prevNum - 1);
  };

  const onChangeText = (e) => {
    setText(e.target.value);
  };

  return (
    <div>
      <button onClick={onIncrease}>+</button>
      <button onClick={onDecrease}>-</button>
      <input type="text" placeholder="text 입력" onChange={onChangeText} />
      <Show getNum={getNum} text={text} />
    </div>
  );
};

export default App;

useCallback을 사용하여 코드를 변경하고 나면 텍스트를 변경할때는 "숫자 변동"이 console창에 찍히지 않는 것을 확인하실 수 있습니다.

그렇다면 위에서 봤던 useMemo와의 차이점이 없다고 느끼실 수도 있습니다. 분명 기능은 엄청 비슷합니다만 어떤 부분이 다르고 어떤 상황에서 사용되는지 알아보겠습니다.

useCallback과 useMemo의 차이점

App.js에서 getNum 함수에 특정한 매개변수 값을 받아 number의 값에 더해주는 코드를 작성한다고 생각해보겠습니다. 그렇다면 다음과 같이 작성할 수 있습니다.

// App.js
const getNum = useCallback(
  (increase) => {
    return number + increase;
  },
  [number]
);

그 후 Show.js에서 getNum 함수에 increase값을 인자로 명시해줍니다.

useEffect(() => {
  setNum(getNum(5));
  console.log("숫자 변동");
}, [getNum]);

그 후 실행시켜보면 num의 default값이 5부터 시작하는 것을 확인하실 수 있습니다.

useMemo를 사용해서 동일한 프로그램 작성이 가능합니다.

// App.js
const getNum = useMemo(
  () => (increase) => {
    return number + increase;
  },
  [number]
);

프로그램을 실행시켜보면 결과값을 동일하게 확인하실 수 있습니다.

useCallback(fn, deps)는 useMemo(() => fun, deps) 와 동일합니다. 그렇기때문에 위 코드처럼 useCallback을 useMemo로 바꿔서 사용이 가능합니다.

정리해보면 useMemo는 함수를 반환하지 않고 함수의 값만 memoization해서 반환합니다. 그에비해 useCallback은 함수 자체를 memoization해서 반환합니다.

useMemo는 함수의 연산량이 많을 때 이전 결과값을 재사용하는 목적이고, useCallback은 함수가 재생성 되는것을 방지함에 차이가 있습니다.

8) useRef

useRef는 저장공간 또는 DOM요소에 접근하기 위해 사용되는 Hook입니다. Ref는 reference 즉, 참조를 뜻합니다.

자바스크립트에서는 특정 DOM 선택을 위해 querySelector나 getElementById 등을 이용했었습니다. React에서도 DOM을 직접 선택하고 조작해야하는 경우가 생길때 useRef를 사용합니다.

기본 문법은 다음과 같습니다.

const ref = useRef(initialValue);

useRef는 보통 다음 두 가지 경우에 많이 사용합니다.

  1. 저장공간(변수 관리)
  2. DOM 요소에 접근

1. 저장공간(변수 관리)

변수 관리에는 보통 useState hook을 사용합니다. 다만 useState hook을 사용하면 state가 변경되고 업데이트될때마다 컴포넌트가 리렌더링됩니다. ref를 사용하면 불필요한 렌더링을 막을 수 있습니다. 또한 컴포넌트가 아무리 렌더링되어도 ref안에 저장되어 있는 값은 변하지 않고 유지됩니다. 그렇기에 변경 시 렌더링을 발생시키지 말아야 하는 값을 다룰 때 유용합니다.

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

const App = () => {
  const [count, setCount] = useState(1);
  const [renderCount, setRenderCount] = useState(1);
  
  useEffect(() => {
    console.log("렌더링 횟수: ", renderCount);
    setRenderCount(renderCount + 1);
  });
  
  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

export default App;

위 코드는 컴포넌트의 렌더링 횟수를 console창에 출력하는 예제입니다. 실행시 useEffect 안에 setRenderingCount가 계속해서 컴포넌트를 리렌더링하기 때문에 무한 루프에 빠지게 됩니다. 이를 useRef를 사용하면 효율적으로 관리할 수 있습니다.

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

const App = () => {
  const [count, setCount] = useState(1);
  const renderCount = useRef(1);
  
  useEffect(() => {
    console.log("렌더링 횟수: ", renderCount.current);
    ++renderCount.current;
  });
  
  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

export default App;

2. DOM 요소 접근

DOM 요소 접근으로는 대표적으로 input 요소에 focus를 줄 때 많이 사용합니다. 예를 들어 로그인 화면에서 id를 입력하는 input을 클릭하지 않아도 자동으로 포커스가 되어있어 키보드로 입력하면 바로 id를 입력하신 경험이 있으실껍니다.

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

const App = () => {
  const inputRef = useRef("");
  
  useEffect(() => {
    inputRef.current.focus();
  })

  return (
    <>
      <input ref={inputRef} />
    </>
  );
};

export default App;

위 코드를 실행시켜보면 input창에 자동으로 focus가 작동하는 것을 확인하실 수 있습니다.

Ref 객체에서 .current의 값은 선택한 DOM을 가리키게 됩니다. 해당 객체의 기능이나 값을 이용하기 위해서는 current를 반드시 붙여주어야 합니다. current의 default값은 useRef를 통해 객체를 선언할 때 넣는 initialValue값이 됩니다.

참조

profile
FE Developer

0개의 댓글