ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React.js] 리액트 훅스 (React Hooks)
    Front-end/React.js 2020. 7. 12. 22:39
    반응형

     

    포스팅 계기

     

    React 16.8 버전부터 Hooks를 지원함으로써, Class를 사용하지 않고 React를 사용할 수 있게 되었다.

    OOP를 선호하지 않거나 bind()의 불편함을 느끼던 개발자들이 해방을 느끼게 되어 많이 쓰이게 되는 듯 하다.

    Legacy 코드를 모두 함수형으로 바꾸기에는 무리가 있지만, 새로 시작하는 프로젝트에서는 주로 쓰일 것이기에 공부할 필요가 있었다.

     


    훅스(Hooks) 등장 배경

     

    Hook는 props, state, context, refs, 그리고 lifecycle와 같은 React 개념에 보다 직관적인 API를 제공할 뿐, React 컨셉을 대체하는 것은 아니다. 그리고 React 팀은 Class 개념을 제거할 계획이 없다고 한다.

    따라서, 기존의 코드를 모두 바꿀 필요가 없으며, 필요에 따라 사용하면 될 듯 하다.

     

    리액트 팀이 말하는 훅스의 동기는 아래와 같다.

     

    상태와 관련된 로직 재사용성

     

    React는 컴포넌트에 재사용 가능한 API를 제공하지 않았다. 따라서, 이를 우회적으로 해결하기 위한 방법으로 render props, high-order components 같은 패턴을 사용했다. 하지만, React 애플리케이션을 본다면, providers, consumers, 고차 컴포넌트, render props 그리고 다른 추상화에 대한 레이어로 둘러 싸인 “래퍼 지옥(wrapper hell)“을 볼 가능성이 높다.

     

    하지만 Hook를 통해 계층 변화 없이 상태 관련 로직을 재사용할 수 있도록 도와준다.

     

     

    클래스는 컴퓨터, 사람 모두 혼동시킨다.

     

    Class가 코드의 재사용성과 코드 구성을 좀 더 어렵게 만들 뿐만 아니라, React를 배우는데 큰 진입장벽이라고 한다. 세 프레임워크 (React.js, Vue.js, Angular.js) 중 러닝허들이 높은 편이라는 피드백을 참고한 것 같다.

    그리고, Javascript에서의 this는 다른 프로그래밍 언어와의 작동방식이 다르기 때문에, 이 작동방식을 알아야만 했다. 그래서 bind를 통해 하나하나 처리해주어야했다. 또한 이벤트 핸들러가 등록되는 방법을 기억해야 한다.

     

    이러한 문제를 해결하기 위해, Hook는 Class없이 React 기능들을 사용하는 방법을 알려준다. 개념적으로 React 컴포넌트는 항상 함수에 더 가깝다. Hook는 React의 정신을 희생하지 않고 함수를 받아들입니다. 


    1. useState

     

    아래의 코드블럭에서 보이는 useState가 바로 Hook 이다. Hook을 호출해 함수 컴포넌트(function component) 안에 count state를 추가했다. 이 state는 컴포넌트가 다시 렌더링 되어도 그대로 유지될 것이다. useState 현재의 state 값(count)과 이 값을 업데이트하는 함수(setCount)를 쌍으로 제공한다.

    useState는 인자로 초기 state 값을 받는다. 아래 예제에서는 count state 초기 값으로 0을 설정하였다.

     

    import React, { useState } from 'react';
    
    function Example() {
      // "count"라는 새 상태 변수를 선언합니다
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }

    2. useEffect

     

    React 컴포넌트 안에서 타이머를 통해서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업을 “side effects”라고 한다. 왜냐하면 이것은 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업이기 때문이다.

    useEffect는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 해준다.  React class life cycle의 componentDidMount  componentDidUpdate, componentWillUnmount와 같은 목적으로 제공되지만, 하나의 API로 통합되었다.

     

    useEffect를 사용하면, React는 DOM을 바꾼 뒤에 “effect” 함수를 실행한다. Effects는 컴포넌트 안에 선언되어있기 때문에 props와 state에 접근할 수 있다.

    React는 기본적으로 매 렌더링 이후에 effects를 실행한다. 하지만, 두번 째 인자로, 디펜던시 목록을 제공하면, 디펜던시 인자가 바뀔 때만 이펙트가 실행된다.

     

    import React, { useState, useEffect } from 'react';
    
    function FriendStatus(props) {
      const [isOnline, setIsOnline] = useState(null);
    
      function handleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
    
      useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      }, [isOnline]);
    
      if (isOnline === null) {
        return 'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }

     

    1. 위의 코드에서처럼 useEffect 함수가 실행될 때는, 친구 리스트를 구독(데이터를 가져옴)하게 된다. 하지만 해당 훅의 생명이 끝날 때는, ChatAPI를 통해 구독했던 데이터를 해제해주어야 한다. 이때, return에서 뒤처리(cleaning up)을 하게 된다.

     

    2. FrinedStatus 컴포넌트가 렌더링 될 때마다 이펙트가 실행될 것이 아니라면 [isOnline]처럼 해당 인자가 업데이트 될 때만, 실행되게 만들 수 있다. (예제의 흐름과는 조금 어울리지 않지만 예시를 위해 넣었다.)


    3. useContext

     

    context 객체(React.createContext에서 반환된 값)을 받아 그 context의 현재 값을 반환한다. context의 현재 값은 트리 안에서 이 Hook을 호출하는 컴포넌트에 가장 가까이에 있는 <ThemeContext.Provider> value prop에 의해 결정된다.

    컴포넌트에서 가장 가까운 <ThemeContext.Provider>가 갱신되면 이 Hook은 그 ThemeContext provider에게 전달된 가장 최신의 context value를 사용하여 렌더러를 트리거 한다.

     

    const themes = {
      light: {
        foreground: "#000000",
        background: "#eeeeee"
      },
      dark: {
        foreground: "#ffffff",
        background: "#222222"
      }
    };
    
    const ThemeContext = React.createContext(themes.light);
    
    function App() {
      return (
        <ThemeContext.Provider value={themes.dark}>
          <Toolbar />
        </ThemeContext.Provider>
      );
    }
    
    function Toolbar(props) {
      return (
        <div>
          <ThemedButton />
        </div>
      );
    }
    
    function ThemedButton() {
      const theme = useContext(ThemeContext);
      return (
        <button style={{ background: theme.background, color: theme.foreground }}>
          I am styled by theme context!
        </button>
      );
    }

     따라서 위의 코드는 theme의 light가 아니라 theme의 dark로 스타일이 적용이 된다.

     


    4. useReducer

     

    useState의 대체 함수이다. 아래의 코드에서 볼 수 있듯이, (state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 현재 state를 반환하게 된다.

    ** 다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우나 다음 state가 이전 state에 의존적인 경우에 보통 useState보다 useReducer를 선호한다고 한다. useReducer자세한 업데이트를 트리거 하는 컴포넌트의 성능을 최적화할 수 있게 하는데, 이것은 콜백 대신 dispatch를 전달 할 수 있기 때문이다.

    function init(initialCount) {
      return {count: initialCount};
    }
    
    function reducer(state, action) {
      switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
          return {count: state.count - 1};
        case 'reset':
          return init(action.payload);
        default:
          throw new Error();
      }
    }
    
    function Counter({initialCount}) {
      const [state, dispatch] = useReducer(reducer, initialCount, init);
      return (
        <>
          Count: {state.count}
          <button
            onClick={() => dispatch({type: 'reset', payload: initialCount})}>
            Reset
          </button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
      );
    }
    

     

    state 초기화

     state의 초기화에는 두 가지 방법이 있는데, 가장 간단하게는 아래의 코드처럼 직접 초기 state 인자를 전달 할 수 있다.

      const [state, dispatch] = useReducer(
        reducer,
        {count: initialCount}
      );

     

    state 초기화 지연

     초기 state를 조금 지연해서 생성할 수도 있다. 이를 위해서 init 함수를 세 번째 인자로 전달한다. 이것은 reducer 외부에서 초기 state를 계산하는 로직을 추출할 수 있도록 한다. 그리고 어떤 액션에 대한 대응으로 나중에 state를 재설정하는 데에도 유용하다.

    첫 번째의 코드에서 Counter 컴포넌트의 initialCount 값을 제공함으로써, 사용할 수 있다.

    function App() {
     return <Counter initialCount={0} />
    }

     


    5. useMemo

     

    메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 동적 계획법의 핵심이 되는 기술이다. 메모아이제이션이라고도 한다.

    간단히 요약하자면, 메모이제이션된 값을 반환한다. (Vue에서 Computed 속성과 비슷한 듯 하다.)

    “생성(create)” 함수의존성 배열을 전달해주면 된다. useMemo는 의존성이 변경되었을 때에만 메모이제이션된 값만 다시 계산 할 것이다. 이로 인한 최적화는 모든 렌더링 시의 고비용 계산을 방지할 수 있게 해준다.

    ** 의존성 배열이 없을 경우 렌더링 할 때마다 계산하게 된다.

     

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

     


    6. useCallback

     

    메모이제이션된 콜백을 반환하게 된다고 한다.

    useMemo와는 다르게 인라인 콜백과 그것의 의존성 값의 배열을 전달한다.

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

    useCallback(fn, deps) useMemo(() => fn, deps)와 같다.

    useCallback 은 결국 useMemo 에서 함수를 리턴할 때 더 편하게 사용 할 수 있게 한다. 숫자, 문자열, 객체 처럼 일반 값을 재사용하기 위해서는 useMemo 를, 그리고 함수를 재사용 하기 위해서는 useCallback 을 추천한다지만 아직 차이가 와닿지는 않는다.


    기타

     

    위의 훅 이외에도, useRef, useImperativeHandle, useLayoutEffetct, useDebugValue과 커스텀 hook를 포함하여 다양한 훅이 존재한다.

    각 훅의 목적을 이해하고, 적시적소에 사용하면 될 것 같다.


    참고

    https://ko.reactjs.org/docs/hooks-intro.html
    https://velog.io/@velopert/react-hooks#9-%EB%8B%A4%EB%A5%B8-hooks
    https://velog.io/@public_danuel/trendy-react-usecontext
    https://haeguri.github.io/2019/10/13/react-hooks-basic/
    반응형

    'Front-end > React.js' 카테고리의 다른 글

    [React.js] 리액트 라이프 사이클(React Lifecycle)  (0) 2020.06.21

    댓글

Designed by Tistory.