React Hooks 공식 문서
https://ko.reactjs.org/docs/hooks-intro.html

React는 v16.8부터 컴포넌트 상태와 컴포넌트 생명주기를 관리할 수 있는 API인 Hook을 제공하고 있다. Hook을 사용하면 함수 컴포넌트에서도 클래스 컴포넌트처럼 상태를 저장할 수 있고 컴포넌트 생명주기에 관여할 수 있다.

React Hooks는 기존 클래스 컴포넌트만 가지고 있던 여러 기능을 지원하고 있어서 함수 컴포넌트를 클래스 컴포넌트처럼 사용할 수 있다. 클래스 컴포넌트와는 다르게 함수 컴포넌트는 모듈화가 쉬워서 앞으로 컴포넌트를 제작할 때는 함수 컴포넌트로 만들 것을 권장하고 있다. 그래도 React는 클래스 컴포넌트에 대한 지원을 끊을 계획이 없다고 하니까 굳이 시간 들여서 기존 클래스 컴포넌트를 함수 컴포넌트로 리팩토링할 필요는 없다.

하지만 아직 함수 컴포넌트에서만 사용할 수 있는 React Hook이 클래스 컴포넌트의 모든 기능을 대체하진 못한다. 대표적으로 클래스 컴포넌트에서 쓰이는 getSnapshotBeforeUpdategetDerivedStateFromError, componentDidCatch 생명주기 메소드는 React Hook에서 지원하지 않는다. 하지만 솔직히 저 생명주기 메소드는 일반적으로 사용하는 경우가 드물어서 함수 컴포넌트가 클래스 컴포넌트의 기능을 거의 대체했다고 봐도 무방하다. 그리고 React에선 앞으로 저 생명주기 메소드를 구현한 Hook도 개발할 예정이라고 한다.

React Hooks

위 그림과 같이 React에선 기본적으로 컴포넌트 상태를 관리할 수 있는 useState와 컴포넌트 생애주기에 개입할 수 있는 useEffect , 컴포넌트 간의 전역 상태를 관리하는 useContext 함수를 제공하고 있다. 그리고 추가로 제공하는 Hook은 기본 Hook의 동작 원리에 기반해서 만들어졌다.

추가 Hook엔 상태 업데이트 로직을 reducer()라는 함수에 따로 분리할 수 있는 useReducer와 컴포넌트나 HTML 엘리먼트를 레퍼런스로 관리할 수 있는 useRef, 그 레퍼런스를 상위 컴포넌트로 전달할 수 있는 useImperativeHandle, 의존 배열에 적힌 값이 변할 때만 값/함수를 다시 정의하는 useMemouseCallback, 모든 DOM 변경 후 브라우저가 화면을 그리기 이전 시점에 동기적으로 실행되는 useLayoutEffect, 사용자 정의(custom) Hook의 디버깅을 도와주는 useDebugValue가 존재한다.

모든 React Hooks는 함수 컴포넌트에서만 사용할 수 있고, 2020년 9월까진 총 10개의 Hook이 존재하지만 더 추가될 수도 있다.

기본 Hooks

useState

useState 공식 문서
https://ko.reactjs.org/docs/hooks-state.html

사용 방법

useState()의 첫번째 매개변수로 state의 초기값을 설정할 수 있다. 그리고 우리가 상태를 바꾸고 싶을 때마다 setState() 함수를 호출하는데 이 함수의 첫번째 매개변수로 넘겨준 값이 새로운 상태로 반영된다. setState()의 첫번째 매개변수로 함수를 넘겨줄 수도 있는데, 그 함수는 이전 상태를 매개변수로 해서 사로운 상태를 반환한다.

컴포넌트 상태

useState는 함수 컴포넌트에서 상태를 생성/수정/저장할 수 있도록 도와준다. useState가 제공하는 상태라는 개념은 JavaScript의 지역 변수와 비슷한 개념으로, 컴포넌트가 마운트되고 언마운트될 때까지 유지되는 값이다. 하지만 지역 변수는 함수가 반환되면 값이 사라진다는 점에서 컴포넌트 상태완 다르다. 즉, 지역 변수는 컴포넌트가 업데이트(렌더링)되고 다시 업데이트되기 전까지 유지되는 값이다. 그래서 Hook이 등장하기 전엔 함수 컴포넌트에선 상태를 따로 저장하고 유지할 수 없었다. 왜 지역 변수 값은 컴포넌트를 업데이트할 때마다 유지되지 않을까?

기본적으로 JavaScript에선 함수 내부에 선언된 지역 변수는 함수가 반환되면 사라진다. 그래서 컴포넌트 내부에서 변경한 변수 값을 화면에 반영하기 위해 컴포넌트를 다시 렌더링하면, 함수 컴포넌트 내부가 다시 실행되면서 기존 변수가 사리지고 새로운 변수(값은 동일)가 다시 정의되서 화면엔 계속 초기값만 표시된다. 그래서 함수 컴포넌트에서 지역 변수는 값을 임시로 저장하는 용도로만 사용하는 것이 좋다. 이와 같은 이유로 지역 변수로는 함수 컴포넌트의 상태를 관리할 수 없기 때문에 useState Hook이 등장했다.

컴포넌트를 렌더링할 때마다 함수 컴포넌트의 내부 코드가 다시 실행되는 이유는 React 가상 DOM과 연관이 있다.

React의 가상 DOM

React는 컴포넌트의 DOM 구조가 변경됐는지 확인하기 위해 클래스 컴포넌트의 render 함수나 함수 컴포넌트를 실행해 반환값을 확인한다. 그 반환값이 이전 반환값과 다르면 컴포넌트 내부 DOM 구조나 상태가 변했다는 뜻이다. React는 이렇게 각 컴포넌트의 변화를 감지하고 이를 가상 DOM에 반영한다.

변화를 감지하는 방식엔 한가지 꼼수가 있는데, 원래 트리 상태가 변하는 걸 감지하고 트리를 탐색하는 알고리즘은 시간복잡도가 매우 높다(이 부분은 자세히 모른다). 하지만 React는 각 컴포넌트에 key prop을 설정해서 변경된 컴포넌트를 바로O(1) 찾을 수 있다. 이 key prop은 React 내부에서 자동으로 설정하지만 Array.prototype.map()을 사용하면서 한번쯤 key prop을 설정하라는 권장사항을 봤을 것이다. 이런 권장사항은 이런 이유 때문에 뜨는 것이다. 즉, 사용자에게 일부 책임을 넘긴 것이다.

어쨌든 React는 이렇게 모든 컴포넌트의 변경 사항이 반영된 최종 가상 DOM 구조를 실제 DOM에 반영하고 브라우저 화면을 한번만 그린다. 원래는 각 컴포넌트에 변경 사항이 있을 때마다 실제 DOM을 변경하고 브라우저 화면을 그렸는데, React는 특정 시점의 모든 컴포넌트의 차이를 모아서 한번만 그리기 때문에 여기서 성능 향상이 있을 수 있다. (반대로 동시에 한 컴포넌트만 바뀌면 성능 향상은 없다?) 가상 DOM은 각 컴포넌트의 변화만 반영하기 위한 일종의 임시로 생성한 DOM이라고 볼 수 있다.

어떻게 사용해야 좋을까

import React, { useState } from 'react';

function App() {
  const [id, setId] = useState('');
  const [password, setPassword] = useState('');
  const [email, setEmail] = useState('');
  const [name, setName] = useState('');

  ...
}

export default App;

위와 같이 useState를 여러 번 사용해서 여러 개의 상태를 관리할 수 있다.

import React, { useState } from 'react';

function App() {
  const [signupData, setSignupData] = useState({
    id: '',
    password: '',
    email: '',
    name: '',
  });

  ...
}

export default App;

또는 위와 같이 useState에 객체를 전달해서 여러 개의 상태를 관리할 수도 있다.

중요한 점은 비슷한 것은 객체로 묶어 놓고, 공통점이 없는 것끼린 분리해서 각 상태의 책임을 분산해야 한다는 것이다. 이래야 모듈화도 쉬워지고 불필요한 큰 객체 생성을 방지할 수 있다. 여기서의 책임은 변화를 의미하는데, 서로 같이 업데이트되는 상태끼린 한 객체로 묶어도 된다는 뜻이다. 예를 들면 마우스 x, y 좌표 상태는 각각 관리하는 것보단 한 객체로 묶어서 관리하는 것이 효율적이고, 위와 같은 회원가입 데이터는 각각 따로 관리하는 것이 좋다. 그리고 만약 상태 업데이트 로직이 복잡하다면 useReducer를 활용해서 해당 컴포넌트로부터 상태 업데이트 로직을 분리하는 것이 컴포넌트 유지 보수 측면에서 좋다.

주의할 점

useState는 상태를 값(value)으로 관리한다. 만약에 상태를 객체로 설정하고 해당 객체에 레퍼런스로 접근해서 필드 값을 변경하면, 객체 필드 값은 변경되겠지만 이를 React가 알 수 없어서 컴포넌트를 자동으로 렌더링해주지 않는다. 이러면 UI의 데이터 일관성이 깨질 수 있다.

useState는 상태가 변경되었는지 확인하기 위해 Object.is()를 사용한다. 이는 ===과 비슷한데

가본 자료형은 값으로 관리되고 const로 선언했기 때문에 다른 값으로 변경할 수 없다. 그래서 useState는 값으로 관리하는 것을 권장하고 객체를 사용한다면 객체 필드를 변경할 땐 반드시 setState 함수를 사용해야 한다....................

useEffect

useEffect 공식 문서
https://ko.reactjs.org/docs/hooks-effect.html

// 1. 매개변수 1개 (함수)
useEffect(() => {
  console.log("component did mount or update");
  return () => {
    console.log("component will unmount or update");
  };
});

// 2. 매개변수 2개 (함수 + 빈 배열)
useEffect(() => {
  console.log("component did mount");
  return () => {
    console.log("component will unmount");
  };
}, []);

// 3. 매개변수 2개 (함수 + 배열)
useEffect(() => {
  console.log("component did (mount or update) and states changed");
  return () => {
    console.log("component will (unmount or update) and states changed");
  };
}, [state, state2, ...]);

사용 방법

useEffect는 위 그림과 같이 사용할 수 있다. useEffect는 2개의 매개변수를 가지는데 첫번째는 함수, 두번째는 배열이다. useEffect 동작 원리는 간단하다. 배열 원소(두번째 매개변수)가 하나라도 바뀔 때마다 컴포넌트 레이아웃 배치와 그리기가 끝난 후 콜백 함수(첫번째 매개변수)가 비동기로 실행된다. 그 후 컴포넌트가 업데이트되기 전에 콜백 함수가 반환하는 함수가 실행된다. 그래서 두번째 매개변수로 항상 변하지 않는 빈 배열을 주면 컴포넌트가 마운트되고 언마운트될 때만 콜백 함수가 실행된다. 콜백 함수 내부는 effect 함수라고 부르고, 콜백 함수가 반환하는 함수는 clean-up 함수라고 부른다.

함수 컴포넌트 생애주기

useEffect는 항상 DOM 업데이트와 레이아웃 배치, 화면 그리기가 모두 완료된 후 실행된다. 그리고 useEffect가 함수 컴포넌트 생애주기에 관여하는 부분은 위 그림과 같다. 컴포넌트는 기본적으로 마운트->업데이트(반복)->언마운트의 생애주기를 가진다. 마운트 상태는 컴포넌트 구조가 실제 HTML DOM에 반영된 상태(화면에 보이는 상태)고, 컴포넌트 DOM 구조나 내용이 변경될 때마다 업데이트 과정을 거치고, 언마운트 상태는 컴포넌트 구조가 실제 HTML DOM에서 제거된 상태(화면에 안 보이는 상태)다.

앞서 말했듯이 React에서 컴포넌트는 매 렌더링 시 컴포넌트 내부 DOM이 변경됐나 확인하기 위해 컴포넌트 내부 변수와 함수가 그 시점의 컴포넌트 props와 상태를 기반으로 다시 계산된다. 그 때 useEffect의 effect 함수와 clean-up 함수가 사용하는 값도 미리 계산돼서 내부 스케줄러에 등록된다? 그래서 위 그림과 같이 컴포넌트 상태가 A에서 B로 변경돼도 A 상태의 clean-up 함수가 실행되고 B 상태의 effect 함수가 실행되는 것이다. 그리고 B 상태의 clean-up 함수는 내부 스케줄러에 의해 다음에 실행된다.

useContext

useContext 공식 문서
https://ko.reactjs.org/docs/hooks-reference.html#usecontext

React Context는 Redux 등과 비슷하게 여러 컴포넌트 간 값을 효율적으로 공유할 수 있는 API를 제공한다. 일반적인 React에서 데이터는 위에서 아래로(상위 컴포넌트에서 하위 컴포넌트로) props를 통해 전달되지만, props를 여러 하위 컴포넌트들에 전달하거나 하위 컴포넌트가 너무 깊이 있을 경우 이 과정이 번거로울 수 있다. 이때 Context를 이용하면 각 컴포넌트에게 데이터를 명시적으로 props로 넘겨주지 않아도 컴포넌트끼리 데이터를 공유할 수 있다. 그리고 Context는 React v16.3 이상에서 기본적으로 지원하기 때문에 따로 설치하지 않아도 된다는 장점이 있다.

Context 생성

// src/userContext.js
import { createContext } from 'react';

const UserContext = createContext({ user: {}, setUser: () => {} });

export default UserContext;

사용자 정보와 그 정보를 변경할 수 있는 함수를 공유하는 React Context를 생성한다. 즉, 이 Context는 user라는 변수와 setUser라는 함수를 가지고 있어서, user에 사용자 정보를 저장하고 setUser 함수로 하위 컴포넌트에서 user 정보를 변경할 수 있다.

Context Provider

// src/App.js
import React, { useState, useEffect } from 'react';
import UserContext from './userContext';

function App() {
  const [user, setUser] = useState(null);

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ChildComponent />
    </UserContext.Provider>
  );
}

export default App;

그리고 ApolloProvider와 비슷하게 Context Provider를 가장 상위 컴포넌트에 감싸준다. 이러면 그 하위 컴포넌트라면 어디에서든지 Context Consumer를 통해 Context에 접근할 수 있다.

Context Provider는 상황에 따라 index.js 파일에서 App 컴포넌트를 감싸도 되고 이렇게 Router 컴포넌트를 감싸도 된다. 여기선 Navigation 컴포넌트와 Route 컴포넌트에서 Context에 접근하기 때문에 App.js<Router>를 감싸도록 설정했다. Context Provider 밖에선 Context에 접근할 수 없다는 점만 주의해서 위치를 정하면 된다.

Context Provider의 value prop엔 Context를 정의할 때 사용한 이름(user, setUser)을 그대로 사용해야 한다.

Context Provider의 하위 컴포넌트라면 React Hooks 중 useContext() (또는 Context Consumer)를 통해 Context 데이터에 접근할 수 있다.

useContext()는 UserContext에 저장된 값을 그대로 반환하는 React Hook이다. 반환된 Context 데이터는 그대로 사용할 수 있다.

useContext 사용 방법

ㅇㄹㄴㅇㅇㄴ

추가 Hooks

useReducer

useState로만

상태를 새롭게 업데이트하는 로직이 복잡할 때 상태 업데이트 로직을 따로 분리할 수 있다.

useRef

useImperativeHandle

useMemo

계산 결과 캐싱

결과가 변하지 않았는데 해당 변수를 다시 계산해야 하는가? 그렇다. 그래서 useMemo을 사용한다.

결과가 변하지 않았다는 것을 어떻게 알 수 있을까? 기본적으로 React는 컴포넌트 내부에서 사용되는 함수는 모두 순수 함수라고 가정하고, 프로그래머도 순수 함수로 코딩해야 한다. 순수 함수는 함수 매개변수가 동일하면 동일한 값이 반환된다고 가정한다. 따라서 힘수 매개변수가 변했는지 보고 결과가 변하지 않았다는 것을 판단할 수 있다.

function createClosure(param) {
  const privateVar = param;
  return () => privateVar + 10;
}

const closure = createClosure(5);
closure();  // 15

const closure2 = createClosure(10);
closure2();  // 20

이전 결과는 클로저를 이용해서 저장한다. 위와 같이 클로저는 내부에 private 상태를 가질 수 있는 함수라고 생각하면 된다. 그래서 이전 결과를 함수 내부에 저장할 수 있다.

useCallback

함수 내용이 변하지 않았는데 함수 컴포넌트 내부에 정의된 함수를 다시 정의해야 하는가? 그렇다. 그래서 useCallback을 제공한다.

useLayoutEffect

useLayoutEffectuseEffect와 원리는 동일하지만 실행되는 시점이 약간 다르다. useEffect는 레이아웃 배치와 화면 그리기가 모두 완료된 후 실행되지만, useLayoutEffect는 레이아웃이 배치된 후와 화면 그리기 전에 실행된다.

브라우저에 화면이 표시되는 과정

브라우저에 화면이 표시되는 과정 이해 필요

useDebugValue

사용자 지정(custom) Hook의 디버깅을 도와준다.

컴포넌트가 렌더링될 때마다

리액트 내부에 선언된 함수는 순수 함수로 바뀐다. 바뀔 수밖에 없다. props랑 컴포넌트 상태를 참조하는데 그건 컴포넌트가 렌더링될 때마다 다시 정의된다.

profile
이유와 방법을 알려주는 블로그

0개의 댓글