React Hooks

React

React는 User Interface를 만드는 라이브러리이다. 개발자에게 Strict한 Data flow를 강제한다는 건 리액트의 큰 강점이다. Container와 Presentational Component를 통한 명확한 구조, Component들이 state와 prop의 변화에 대응하는 Strict한 Data 흐름을 강제함으로써 좀 더 논리적인 UI logic 을 만들기 쉽다.

React의 기본적인 개념은 UI의 작은 부분들은 state의 변화에 “react” 할 수 있다는 것이다. 기존에는 class를 통해서 이런 흐름을 만들어 냈었다.

import React, { Component } from "react";

export default class Button extends Component {
  constructor() {
    super();
    this.state = { buttonText: "Click me, please" };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { buttonText: "Thanks, been clicked!" };
    });
  }

  render() {
    const { buttonText } = this.state;
    return <button onClick={this.handleClick}>{buttonText}</button>;
  }
}

예를 들어, User가 버튼을 누르면 => this.setState를 통해서 component의 내부적인 상태가 변경된다. 버튼 내부의 텍스트는 이런 변화에 “react”하고 업데이트 된 텍스트를 받는다.

하지만, React Hook을 이용해 함수형 컴포넌트에서도 위와 같이 상태관리를 할 수 있다. 훨씬 쉽고 간결하게.

useState

useState 는 React 자체적으로 제공하는 함수이다.

import React, { useState } from "react";

일단 React 를 쓰면 두개의 값으로 destructure 할 수 있다. 그 ‘상태’의 실제 값, 상태를 변경하는 함수(state updater) 두 가지를 반환한다.

const [buttonText, setButtonText] = useState("Click me, please")

useState로 전달되는 argument는 실제 초기값이고, 그 데이터가 변경될 값이다.

useState를 이용하면 아까 그 Class Component는 이런식으로 바꿔볼 수 있다.

import React, { useState } from "react";

export default function Button() {
  const [buttonText, setButtonText] = useState("Click me, please");

  return (
    <button onClick={() => setButtonText("Thanks, been clicked!")}>
      {buttonText}
    </button>
  );
}

useEffect

useEffect는 기존의 { componentDidMount, componentDidUpdate, componentWillUnmount } 를 한번에 해결할 수 있는 API이다.

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

export default function DataLoader() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch("http://localhost:3001/links/")
      .then(response => response.json())
      .then(data => setData(data));
  });

  return (
    <div>
      <ul>
        {data.map(el => (
          <li key={el.id}>{el.title}</li>
        ))}
      </ul>
    </div>
  );
}

DataLoader라는 함수형 컴포넌트를 만들어봤을때 서버에서 response를 받아서 setData를 통해서 data를 받아와서, <li>로 각 데이터를 뿌려주는 로직이다.

이때 주의해야할 점은, 이렇게만 useEffect를 쓰게 되면, 컴포넌트가 새로운 prop을 받는다던가, state가 변경될때마다 useEffect 내의 함수가 호출된다는 것이다.

useEffect(() => {
   fetch("http://localhost:3001/links/")
     .then(response => response.json())
     .then(data => setData(data));
}, []); // << super important array

이는 이런식으로 두번째 인자에 빈 어레이를 넘기는 방법으로 해결할 수 있는데, 두번째 인자는 dependency, 즉 useEffect가 다시 수행할 지 여부를 결정하는 의존 변수들이 들어간다. 만약 이 배열이 비어있으면, 컴포넌트가 뜰때 딱 한번만 수행될 것이다.

useEffect 의 Effect cleanup

Timer, listener, 지속적인 Connection들(websocket이나 그런 친구들)은 Javascript memory leak의 가장 흔한 문제 원인들이다.

useEffect(() => {
  const socket = socketIOClient(ENDPOINT);
  socket.on("FromAPI", data => {
    setResponse(data);
  });
}, []);

예를 들어 위와 같은 상황에서 component가 DOM 으로부터 unmount할때도 connection이 열린채로 유지된다는 것이 문제다.

useEffect(() => {
  const socket = socketIOClient(ENDPOINT);
  socket.on("FromAPI", data => {
    setResponse(data);
  });
  
  return () => socket.disconnect();
  
}, []);

이런식으로 useEffect 내에서 effect를 cleanup 할 수 있는 function을 return 할 수 있다. 이제 connection 은 component가 unmount될때 close될것으로 기대할 수 있다.

Custom Hook

자주 사용하는 로직들을 묶어서 custom hook을 만들어볼 수 있다. 다른 hook들 처럼 custom hook 은 보통 use로 시작하는 JS 함수이다.

import { useState, useEffect } from "react";

export default function useFetch(url) {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch(url)
      .then(response => response.json())
      .then(data => setData(data));
  }, []);

  return data;
}
import React from "react";
import useFetch from "./useFetch";

export default function DataLoader(props) {
  const data = useFetch("http://localhost:3001/links/");
  return (
    <div>
      <ul>
        {data.map(el => (
          <li key={el.id}>{el.title}</li>
        ))}
      </ul>
    </div>
  );
}

Async/Await 를 useEffect와 함께 쓰기

javascript async 함수는 항상 promise를 return 한다. 이때 useEffect는 cleanup 함수만 return할 수 있으므로, 아래와 같은 코드는 에러가 발생한다.

useEffect(async () => {
  const response = await fetch(url);
  const data = await response.json();
  setData(data);
}, []);

즉, useEffect 내에서는 Promise를 Return할 수 없으므로, async, await 를 useEffect와 함께 쓰기 위해서는,

import { useState, useEffect } from "react";

export default function useFetch(url) {
  const [data, setData] = useState([]);

  async function getData() {
    const response = await fetch(url);
    const data = await response.json();
    setData(data);
  }

  useEffect(() => {
    getData();
  }, []);

  return data;
}

이런식으로 async 함수를 별도로 만들어서 사용하여 주도록 한다.

useReducer

useReducer는 또다른 훅인데, React component의 좀 더 복잡한 상태 변화를 다루는데 유용하다. useReducer는 Redux의 reducer, action, dispatch 컨셉들을 가져온 것이다.

export function useFetch(endpoint) {
  const [data, dispatch] = useReducer(apiReducer, initialState);

  useEffect(() => {
    dispatch({ type: "DATA_FETCH_START" });

    fetch(endpoint)
      .then(response => {
        if (!response.ok) throw Error(response.statusText);
        return response.json();
      })
      .then(json => {
        dispatch({ type: "DATA_FETCH_SUCCESS", payload: json });
      })
      .catch(error => {
        dispatch({ type: "DATA_FETCH_FAILURE", payload: error.message });
      });
  }, []);

  return data;
}
const [data, dispatch] = useReducer(apiReducer, initialState);

useReducer를 이용하면, data와 action을 dispatching(:특정 목적을 가지고 보내다)할 함수를 얻는다. 이 함수를 호출하면서 action들을 Dispatch시킬 수 있다.

useEffect(() => {
    // dispatch an action
    dispatch({ type: "DATA_FETCH_START" });

    fetch(endpoint)
      .then(response => {
        if (!response.ok) throw Error(response.statusText);
        return response.json();
      })
      .then(json => {
        // dispatch an action on success
        dispatch({ type: "DATA_FETCH_SUCCESS", payload: json });
      })
      .catch(error => {
        // dispatch an action on error
        dispatch({ type: "DATA_FETCH_FAILURE", payload: error.message });
      });
	  }, []);

이 Action들은 Reducer 함수에서 다음 상태를 연산하기 위해 사용된다. 일단 시작할때, loading중을 활성화시키는 DATA_FETCH_START를 dispatch 시키고, 성공시, 실패시 각각 필요한 Action을 dispatch시킨다

mport { useEffect, useReducer } from "react";

const initialState = {
  loading: "",
  error: "",
  data: []
};

function apiReducer(state, action) {
  switch (action.type) {
    case "DATA_FETCH_START":
      return { ...state, loading: "yes" };
    case "DATA_FETCH_FAILURE":
      return { ...state, loading: "", error: action.payload };
    case "DATA_FETCH_SUCCESS":
      return { ...state, loading: "", data: action.payload };
    default:
      return state;
  }
}

export function useFetch(endpoint) {
  const [data, dispatch] = useReducer(apiReducer, initialState);

  useEffect(() => {
    dispatch({ type: "DATA_FETCH_START" });

    fetch(endpoint)
      .then(response => {
        if (!response.ok) throw Error(response.statusText);
        return response.json();
      })
      .then(json => {
        dispatch({ type: "DATA_FETCH_SUCCESS", payload: json });
      })
      .catch(error => {
        dispatch({ type: "DATA_FETCH_FAILURE", payload: error.message });
      });
  }, []);

  return data;
}

Conclusion

결론: React hook은 여러모로 prop rendering이나 HOC를 대체할 수 있고, Stateful 한 로직을 공유하는 데 굉장히 유용하다

Reference