Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

백엔드 개발 블로그

React Hook 정리 및 Tips 본문

React

React Hook 정리 및 Tips

베꺼 2022. 4. 17. 16:23

React 17 버전을 기준으로 작성된 글입니다. (2022/04)

React Hook

  • 기존 리액트에서는 간단한 형태의 컴포넌트는 함수로 표현할 수 있었지만, 상태 등을 저장하기 위해서는 클래스로 작성해야 했다.
  • React Hook은 useState, useEffect와 같은 메커니즘을 추가하여 함수 형태로도 복잡한 컴포넌트를 작성할 수 있도록 했다.
  • 기존 클래스 구조의 컴포넌트에 있었던 몇가지 문제들을 해결하고자 하였다.

기존의 클래스 컴포넌트와의 호환성

  • 기존 리액트 코드를 그대로 사용할 수 있다.
  • 기존 리액트 컨셉과 다르지 않으며, 오히려 더 직관적인 부분이 있다.
  • 클래스 컴포넌트와 hook이 사용된 함수형 컴포넌트를 섞어 써도 아무 문제가 없다.
  • 클래스 컴포넌트를 fade out할 계획 또한 없다.
  • 하지만 신규 컴포넌트를 만든다면 hook을 사용하는 것을 권장한다.
  • HOC나 render props 들도 hook이 사용된 함수형 컴포넌트에 그대로 사용해도 무방하다.
    • 하지만 hook을 사용하면 대부분의 경우 HOC 등을 대체할 수 있다

Hook의 종류

  • 아래 두가지 hook이 핵심
    • useState: 상태 관리
    • useEffect: componentDidMount와 같은 컴포넌트 라이프사이클 및 다양한 event / subscriber 관리
  • 아래 두가지 hook은 성능 개선을 위한 hook
    • useMemo: 캐싱을 통한 성능 개선
    • useCallback: useMemo의 함수 버전
  • 아래는 복잡한 state를 편리하게 관리하기 위한 hook
    • useReducer: redux의 reducer와 유사하게 state 관리 가능
  • 기타 기능을 위한 hook
    • useRef: Low-level HTML DOM에 접근하는 ref 기능을 함수형 컴포넌트에서 사용하기 위한 hook
    • useContext: 전역 context 접근

State Hook

const [age, setAge] = useState(42);

위와 같이 선언할 경우, age 변수에서 현재 값을 얻어올 수 있고, setAge(newValue)를 호출하여 값을 업데이트할 수 있다. default 값은 42가 된다.

const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

위와 같이 object도 설정 가능하다.
주의: state의 값은 항상 immutable 로 취급할 것. todos.text = 'Another Value`와 같이 mutable하게 사용하면 실수하기 쉽다

setAge(oldAge => oldAge + 1);

기존 setState와 동일하게 이전 값을 인수로 받는 함수도 작성이 가능하다.

코드 예

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Effect Hook

기존 class 컴포넌트에서 componentDidMount, componentDidUpdate 등 각종 라이프사이클 메소드를 대체한다.
제일 복잡한 hook인 것 같다...

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

위와 같이 componentDidMount, componentDidUpdate로 표현되던 부분을...

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

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

위와 같이 useEffect로 구현할 수 있다. 위와 같이 선언된 useEffect는 component가 처음에 마운트되고, 업데이트될 때마다 동작한다.

useEffect(() => {
    console.log("value has changed!")
}, [value]);

useEffect의 두번째 인자는 dependency를 나타낸다. useEffect에 두번째 인자가 존재한다면, effect는 dependency 배열에 있는 변수가 업데이트되었을 때에만 실행된다.
주의: hook에서 사용하는 모든 dependency는 shallow 비교

useEffect(refreshPods, []);

위와 같이 빈 배열을 넘길 경우, 배열의 값은 바뀔 일이 없기 때문에 useEffect는 처음 한번만 실행된다. 즉, componentDidMount와 유사하게 동작한다.

useEffect cleanup

useEffect(() => {
    console.log("Now value is " + value);
    return () => {
        console.log("value is no longer" + value);
    };
}, [value]);

useEffect의 effect 함수에서는 return 값으로 또다른 함수를 넘길 수 있다. 이를 cleanup이라고 하는데, effect가 또다시 실행될 때 이전에 실행됐던 내용을 정리할 수 있다.

예를 들어, value의 초기 값이 1이었다고 하자. 그러면 아래와 같은 로그가 최초에 출력될 것이다.

Now value is 1

이후 value의 값이 2로 바뀐다면 아래와 같은 로그가 남는다.

Now value is 1
value is no longer 1
Now value is 2

즉 기존에 실행되었던 내용을 정리하는 데에 사용할 수 있어서, 아래와 같이 timeout을 설정하거나, 혹은 subscriber 등을 설정할 때 유용하게 사용할 수 있다.

useEffect의 호출 순서

effect를 사용할 때에는 effect는 항상 일단 한 번 렌더링이 수행된 후에 호출된다는 것을 유의해야 한다.

 

아래 예제를 보자.

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

function Example() {
  const [items, setItems] = useState(['a', 'b']);
  const [numItem, setNumItem] = useState(items.length);

  useEffect(() => {
    setNumItem(items.length);
  }, [items]);

  return (
    <div>
      { items.map(item => <p>{item}</p> }
      <p>총 item의 갯수: {numItem}</p>
    </div>
  );
}

위 코드에서 초기 상태에서는 아래와 같이 보여질 것이다.

a
b
총 item의 갯수: 2

위 코드에서 items에 'c'가 더 추가되면 어떻게 될까?

a
b
c
총 item의 갯수: 2

위 화면이 한번 출력된 다음에

a
b
c
총 item의 갯수: 3

위와 같이 한번 더 렌더링이 수행된다. 즉, 일관적이지 않은 상태를 갖게 되며 페이지가 매끄럽게 변화하지 않고 깜빡이는 듯한 느낌을 준다.

 

이렇듯 useEffect는 API 호출, 콜백 등 async 로직에만 사용하는 것이 좋고 렌더링에 영향을 주는 sync 로직에는 사용해선 안된다.

 

이를 고치려면 useEffect 없이 바로 set 함수를 호출하면 된다.

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

function Example() {
  const [items, setItems] = useState(['a', 'b']);
  const [numItem, setNumItem] = useState(items.length);

  if (items.length != numItem) setNumItem(items.length);

  return (
    <div>
      { items.map(item => <p>{item}</p> }
      <p>총 item의 갯수: {numItem}</p>
    </div>
  );
}

위와 같이 render 도중에 setter가 호출되면, render 없이 state가 업데이트되고 함수 전체가 다시 한번 실행된다.


이런 방식을 사용할 때는 주의해야 하는데, 무한 루프가 발생할 수 있고 구조가 직관적이지 않기 때문이다.


근데 이 경우에는 setter가 좀 불필요해보인다.

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

function Example() {
  const [items, setItems] = useState(['a', 'b']);

  const numItem = items.length;

  return (
    <div>
      { items.map(item => <p>{item}</p> }
      <p>총 item의 갯수: {numItem}</p>
    </div>
  );
}

가장 좋은 방법은 setter를 안 쓰는 것이다. 다른 state로부터 계산되는 값은 state로 저장하지 않는 것이 제일 좋다.

Memo Hook

"다른 state로부터 계산되는 값은 state로 저장하지 않는 것이 제일 좋다."라고는 했지만,
그 계산의 시간 복잡도가 높아서 전체적인 성능을 저하할 우려가 있으면 어떻게 할까?

 

이 때 계산된 값을 캐시(memoize)하기 위해 useMemo를 사용한다.
(Memo는 알고리즘 dynamic programming 기법에서 주로 쓰이는 메모이제이션의 Memo다)

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

function Example() {
  const [items, setItems] = useState(['a', 'b']);

  const numItem = useMemo(() => items.length, [items];

  return (
    <div>
      { items.map(item => <p>{item}</p> }
      <p>총 item의 갯수: {numItem}</p>
    </div>
  );
}

위와 같이 useMemo로 감싸면, items가 업데이트될 때에만 items.length에 접근한다.

useMemo(() => {
    const requestBody = {
        audioId: audioId,
        callbackParam: isNotBlankString(callbackParam) ? callbackParam : null,
        meta: meta
    };

    onChange(requestBody);
}, [audioId, callbackParam, meta]);

위와 같이 value를 캐시하는 용도 뿐만 아니라 useEffect처럼 사용도 가능하다.


다른 점은, useEffect는 일단 한 번 렌더링이 수행된 후에 호출되며
useMemo는 렌더링 중에 호출된다는 것이다.

 

렌더링에 영향을 주는 state의 setter 등을 호출할 필요가 있다면 useEffect가 아닌 useMemo를 사용해야 한다.
위에서도 설명했지만 주의해야하는데, 무한 루프가 발생할 수 있고 구조가 직관적이지 않기 때문이다.

 

또, React에서는 shallow한 비교를 자주 사용하는데
이 때 변수의 레퍼런스를 일정하게 유지하는 데에도 useMemo를 유용하게 쓸 수 있다.

 

useMemo는 어디까지나 성능 개선을 위한 장치이기 때문에, 꼭 필요한 경우에 사용해야 한다.
무분별하게 사용하면 오히려 성능이 저하되고 가독성을 저해한다.

Callback Hook

useMemo를 사용해서 값을 캐시할 수 있었다.

만약 함수를 캐시하고 싶다면, useMemo의 함수 버전인 useCallback을 사용할 수 있다.

 

물론 useMemo도 아래와 같이 함수를 캐시할 수 있다.

const newFunction = useMemo(() => {
    return () => {
        oldFunction();
        doSomeOtherThing();
    }
}, [oldFunction]);

useCallback을 하면 깔끔해진다.

const newFunction = useCallback(() => {
    oldFunction();
    doSomeOtherThing();
}, [oldFunction]);

react에서는 콜백함수를 다룰 때가 많다보니 useCallback이란 이름이 붙은 게 아닌가 짐작된다.


useMemo와 마찬가지로 무분별하게 사용해선 안된다.

Custom Hook

  • 여러 hook이 포함된 로직을 재사용하거나, 가독성을 올리기 위해선 어떻게 할까?
import React, { useState, useEffect } from 'react';

function Example() {
  const [items, setItems] = useState(['a', 'b']);

  const numItem = useMemo(() => items.length, [items];

  onEffect(() => console.log("item has changed!"), [items]);

  return (
    <div>
      { items.map(item => <p>{item}</p> }
      <p>총 item의 갯수: {numItem}</p>
    </div>
  );
}

위의 item 상태 관리 및 numItem 계산을 다른 곳에서 재사용하고 싶다면?

function useItems() {
  const [items, setItems] = useState(['a', 'b']);

  const numItem = useMemo(() => items.length, [items];

  onEffect(() => console.log("item has changed!"), [items]);

  return {
    item, numItem
  }
}

위와 같이 세가지 hook을 하나의 함수로 묶어서 useItems라고 명명해보자.

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

function Example() {
  const {numItem, items} = useItems();

  return (
    <div>
      { items.map(item => <p>{item}</p> }
      <p>총 item의 갯수: {numItem}</p>
    </div>
  );
}

위와 같이 useItems와 관련된 로직을 여러 컴포넌트에서 손쉽게 재사용할 수 있다.

단순히 별도의 함수로 빼서 묶은 것이기 때문에 리턴 타입이나 인자는 자유롭게 사용할 수 있다.

 

이와 같이 여러 hook을 별도의 함수로 선언해둔 것을 Custom Hook이라고 하며,
일반적으로 useXXX과 같이 작명한다. (hook이 포함되어 있는 로직인지, 일반 자바스크립트 로직인지 구분이 쉽다)

hook과 관련된 logic과 일반 javascript 함수를 구분하기 위한 추상적인 개념이라고 할 수 있다.

Hook 사용 시 주의할 점 2가지

Hook은 항상 top-level에서 호출되어야 한다

Hook은 반복문, 조건문, nested function 등에서 사용하면 안된다.

 

왜??

 

아래와 같은 state가 있다고 가정해보자.

const [item, setItem] = useState();
const [age, setAge] = useState();
const [name, setName] = useState();

React 프레임워크는 어떻게 item과 age를 구분할까?
문자열을 받는 것도 아니고, "item"이나 "age" 같은 단어는 변수명이라 스코프 외부에서 접근할 방법도 없는데?

 

정답은... useState()의 호출 순서다.

React는 useState 내부에 저장하고 있는 값이 무엇인지는 관심 없고, 첫번째 state, 두번째 state로만 구분한다.

 

만약 중간에 if 문이 들어가서 호출 여부가 달라지면 어떻게 될까?

const [item, setItem] = useState();
if (false) const [age, setAge] = useState();
const [name, setName] = useState();

React 입장에선 첫번째 호출된 건 item, 두번째 호출된 건 age였는데,
중간에 조건문에 false라면 순서가 뒤섞이게 된다.

 

이 경우 유저는 기대했던 name 값이 아니라 age 값을 받게 된다.

 

for문 또한 호출되는 횟수가 달라질 수 있고,
nested function도 항상 같은 순서로 호출되도록 하기가 쉽지 않다.

 

따라서 hook은 top-level에서만 사용해야 한다.

Hook은 리액트 함수형 컴포넌트에서만 사용해야 한다.

Hook은 리액트 함수형 컴포넌트에서만 사용해야 하며, 다른 일반 자바스크립트 함수나 클래스형 컴포넌트에서 실행하면 안된다.

 

커스텀 훅은?
함수형 컴포넌트가 커스텀 훅을 호출한다면 내부 hook을 호출하는 context는 여전히 함수형 컴포넌트이기 때문에 상관 없다.

'React' 카테고리의 다른 글

React Hook을 이용하여 카카오 애드핏(Adfit) 연동하기  (0) 2022.04.17
Comments