본문 바로가기

React

[ React ] Hook - 공식문서 뜯어보기

반응형

Hook이란? 

Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 '연동(hook into)'할 수 있게 해주는 함수입니다. 

 

공식문서에서 Hook을 위와 같이 기술하고 있다. Hook이 없던 시절에는 함수 컴포넌트로 state나 생명주기 기능을 조작할 수 있는 방법이 없었기 때문에 클래스를 강제적으로 사용해야만 했다. 그런데 Hook이 등장함으로써 함수 컴포넌트 내부에서 state와 생명주기 기능을 조작할 수 있게 되었다. 즉, React는 Hook의 등장 이후로 더이상 class에 의존하지 않게 되었다고 할 수 있다.

 

Hook은 크게 빌트인 훅과 커스텀 훅으로 나뉜다. 빌트인 훅은 React에서 제공하는 훅이고, 커스텀 훅은 개발자가 새롭게 생성하는 훅이다. 참고로 Hook 이름은 관습적으로 'use'로 시작한다. 아래 그림은 Hook의 생명주기 다이어그램이다. use로 시작하는 함수는 전부 빌트인 훅이다. 이 글에서는 useState, useEffect, useMemo, useContext 정도만 알아본다. useRefuseReducer는 링크를 클릭하면 관련 글을 볼 수 있다. 

 

https://wavez.github.io/react-hooks-lifecycle/

 

Hook 규칙

아까 정의에서 살펴봤듯이 Hook은 '함수'다. Hook은 강력한 기능을 제공하지만 아래 두 가지 규칙을 지켜야 한다. 

1. 함수 컴포넌트에서만 호출 (일반적인 JavaScript 함수에서 호출하면 안 됨)
2. 컴포넌트의 최상위에서만 호출 

 

Hook API

1. useState

// 구문
const [state, setState] = useState(initialState);

- 상태 유지 값과 그 값을 갱신하는 함수를 반환한다. 위 코드는 배열 구조분해 할당을 적용한 모습이다. 

- initialState는 최초 렌더링을 하는 동안 반환되는 값이다.

- setState 함수는 state를 갱신할 때 사용한다. 

 

 

> setState의 두 가지 사용법 + initialState의 함수적 초기화

// 일반 갱신
setState(newState)

// 함수적 갱신
setState(cur => cur + 1)

// initialState의 함수적 초기화
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

- 일반 갱신의 경우 값이 대체되는 것으로 이해하면 된다.

- 함수적 갱신의 경우 첫번째 인자가 이전 상태, 함수의 반환값이 갱신되는 상태값이다.

- 초기 상태가 약간 무거운 작업이라면, 함수로 받을 수 있다. initialState가 콜벡 형태면 초기 렌더링 될 때만 실행되고, 일반 함수라면 re-render 시에도 실행된다. 

 

 

> 주의

const [state, setState] = useState({ a: 1, b: 2, c: 3});

// c만 7로 바꾸는 경우
setState(prevState => {
  return {...prevState, c: 7};
});

- state는 갱신된 값으로 완전히 대체되기 때문에 값의 일부만 갱신하더라도 전체를 복제해야 한다.

 

 

 

2. useEffect

 

// 구문
useEffect(didUpdate)

- useEffect에 전달된 함수(didUpdate)는 화면에 렌더링이 완료된 후에 실행된다. 

- didUpdate는 re-rendering마다 실행된다. 

- didUpdate가 화면에 렌더링이 완료되기 전에 실행되도록 하기 위해서는 useLayoutEffect Hook을 사용하면 된다. 

 

 

> clean-up 함수

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // Clean up the subscription
    subscription.unsubscribe();
  };
});

- 컴포넌트가 화면에서 제거될 때 didUpdate 내부에서 반환하는 함수를 실행시킨다.

 

 

> 특정 값이 변할 때만 실행시키려면

useEffect(didUpdate, [listen1, listen2])

- didUpdate 함수는 listen1 또는 listen2 값이 변할때마다 실행된다. 

 

 

> 초기 렌더링만 실행, 이후 절대로 실행x

useEffect(didUpdate, [])

 

이런 형식은 주로 서버에 요청을 보낼때 사용한다. 주의할 점은 didUpdate에서 async/await 구문을 쓰려면 외부에서 async 함수를 따로 선언해준 뒤에 didUpdate 안에서 호출해야 한다. didUpdate 자체에 async를 붙이면 개발자 도구의 콘솔에서 경고 메시지를 볼 수 있을 것이다. 자세한 내용은 여기서 확인할 수 있다. 정리하면 아래와 같다.

 

// 틀린 예시
useEffect(async() => { await ~ }, [])


// 올바른 예시
async function foo() { await ~ }

useEffect(() => { foo() }, [])

 

 

> 처음 렌더링때 실행x, 이후에 어떤 특정 순간에만 실행

function Component() {
	const [executeNow, setExecuteNow] = useState(false);

	useEffect(() => {
        if(executeNow){
            //do whatever you want!!: 실행함수
            setExecuteNow(false);
        }
    }, [executeNow])

    return(
        <button onClick={() => setExecuteNow(true)}>실행하기!</button>
    );
}

 

 

 

3. useMemo

// 구문
const memoizedValue = useMemo(() => compute(a, b), [a, b]);

- 메모이제이션된 값을 반환한다.

- memoizedValue에 저장되는 값은 useMemo의 콜벡 함수 반환값이다.

- 초기 렌더링시 compute 결과값을 메모리에 저장해둔다.

- re-렌더링시 compute의 a, b가 바뀔때만 compute 함수를 다시 호출한다. 

- compute 함수를 useEffect에서 쓰면 렌더링 할때마다 compute 함수가 호출된다.

 

 

> useMemo와 거의 비슷한 useCallback

// 아래 둘은 완전 동일함
useCallback(fn, deps)
useMemo(() => fn, deps)

 

 

 

4. useContext

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language. 

 

공식문서에서 그대로 가져왔다. context는 리액트 컴포넌트 트리 안에서 데이터를 전역으로 공유할 수 있도록 고안된 방법이다. 대표적인 예시로 로그인 여부, 스타일링 테마, 페이지 언어(kor, en) 등이 있다.

 

context가 정말 필요한 예로 다크 모드를 구현하는 상황을 가정해보자. 우선 모든 컴포넌트마다 다크모드인지 판별하기 위해 최상위 컴포넌트에서  'isDark'라는 상태값을 useState로 생성해준다. 그다음 할 일은? 모든 하위 컴포넌트에 props로 isDark를 넘겨준다. 아래와 같이 말이다. 

 

const App = () => {
  const [isDark, setIsDark] = useState(false);

  return (
    <div>
      <Header isDark={isDark} />
      <Content isDark={isDark} />
      <Footer isDark={isDark} setIsDark={setIsDark}/>
    </div>
  )
}

 

isDark는 거의 모든 컴포넌트가 알아야 하는 값이므로 Header, Content, Footer 하위 컴포넌트에도 isDark를 props로 넘겨줘야 한다. 이렇게 prop이 하위 컴포넌트에 끊임없이 상속되는 현상을 'prop drilling'이라고 한다. 이러한 상황에서 context는 아주 훌륭한 대안이 될 수 있다. 

 

context를 사용하는 방법은 간단하다. 아래 세 단계만 밟으면 된다. 

1. createContext로 context 객체 생성
2. context를 구독하고자 하는 컴포넌트를 MyContext.Provider로 감싼다.
3. 구독한 컴포넌트의 자식 컴포넌트에서 useContext로 context에 접근한다. 

 

MyContext를 구독하고 있는 컴포넌트에서 useContext(MyContext)를 호출하면 Provider의 props 가 반환된다. 아래 Page 컴포넌트 함수에서 useContext로 불러온 data와 MyContext.Provider에서 넘겨준 data는 동일한 값이다. createContext의 인자는 MyContext.Provider로 감싸지 않은 상태에서 useContext(MyContext)를 호출한 경우 반환하는 값이다. 

 

const MyContext = React.createContext(null);

<MyContext.Provider value={{data}}>
  <Page />
</MyContext.Provider>


// Page 컴포넌트
const Page = () => {
  const { data } = useContext(MyContext)
    
  ...
}

 

 

접은글을 펼치면 다른 예시 코드를 볼 수 있다.

더보기
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() {
  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>
  );
}

 

그러나 context는 폴더를 따로 만들어서 관리하는게 유지보수에 좋다. 접은글에 있는 예시 코드는 아래와 같이 리팩토링 할 수 있다. 코드가 좀 길어서 이 역시 접은글을 펼치면 볼 수 있다. 

더보기
// 경로: ./context/ThemeContext.js

import { createContext } from "react";

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

export const ThemeContext = createContext(themes.light);

 

// App.js

import { ThemeContext } from './context/ThemeContext';

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

 

// ThemedButton.js

import { useContext } from 'react'
import { ThemeContext } from './context/ThemeContext';

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

 

Custom Hook

커스텀 훅은 특별한게 아니다. 자바스크립트에서 반복되거나 복잡한 로직들을 함수화 한 경험이 있을 것이다. 커스텀 훅도 마찬가지다. 컴포넌트 함수 내에서 반복되거나 복잡한 부분을 따로 떼어낸 것이 커스텀 훅이다. 커스텀 훅은 컴포넌트와 달리 jsx 구문이 없기 때문에 props 개념이 없다. 따라서 일반 함수처럼 파라미터를 받고 내부에서 다른 훅들로 반복되는 로직을 짜면 된다. 

 

좋은 예시로는 화면의 가로, 세로 크기를 반환하는 훅이 있다. 아래 코드를 보자. 복잡해 보이지만 내용은 간단하다. 윈도우 창을 resize 할때마다 가로, 세로 상태를 업데이트 해주는 커스텀 훅이다. 참조에 사이트 링크가 있으니 한번 보는 것을 추천한다. 

 

import { useState, useEffect } from "react";
// Usage
function App() {
  const size = useWindowSize();
  return (
    <div>
      {size.width}px / {size.height}px
    </div>
  );
}
// Hook
function useWindowSize() {
  // Initialize state with undefined width/height so server and client renders match
  // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });
  useEffect(() => {
    // Handler to call on window resize
    function handleResize() {
      // Set window width/height to state
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    // Add event listener
    window.addEventListener("resize", handleResize);
    // Call handler right away so state gets updated with initial window size
    handleResize();
    // Remove event listener on cleanup
    return () => window.removeEventListener("resize", handleResize);
  }, []); // Empty array ensures that effect is only run on mount
  return windowSize;
}

 

 

커스텀 훅들은 일반적으로 아래와 같이 'hooks' 라는 파일에 따로 관리한다.

src>hooks>customHook.js
src>utils>hooks>customHook.js

 

 

 

<참조>

https://ko.reactjs.org/docs/hooks-intro.html   

https://usehooks.com/useWindowSize/    

반응형