React Hook(기본 제공)

설탕유령·2022년 9월 20일
0

개요

React를 사용하면서 useEffect를 자유 사용하게 된다.
useEffect가 Hook이라는 사항은 인지하지만,
Hook에 정확한 범위가 무었인지, 그리고 Hook이라는 기술 자체의 장점을 정리해보고자 한다.

Hook 이란?

Hook은 React 버전 16.8부터 React 요소로 새로 추가 되었습니다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있습니다. - React 공식문서

hook은 class 방식 대신 function component에서 state를 사용 할 수 있는 기능으로 시작했다.
과거에 Class 방식과 Function을 나누는 가장 큰 차이점은 state 및 Life Cycle method를 다룰 수 있느냐였다.

Class 방식에서는
Mounting, Updation, Unmounting으로 이루어진 Life Cycle에서
constructor, render 등의 생명주기 관리 메소드를 호출해 상태를 관리해왔다.

다만 function component 방식에서는 사용을 하지 못했기 때문에 function component가 더 빠르고 간결하더라도 Class 방식을 사용해왔다.
하지만 Class 방식은 상대적으로 다음과 같은 문제점을 가지고 있었다.

  • component간에 로직의 재사용이 어려움
  • 길어지는 코드로 인해서 복잡성 증가

Hook이 나오고 나서는 Class 방식을 고집할 이유가 사라졌고, function component 방식을 주로 사용하게 되었다.

Class/ Function 방식 코드 비교

Class 방식
import React, { Component } from 'react';

class ClassComponent extends Component {
  state = {
    count: 0,
  };

setCount(num) {
  this.setState({
    count: num,
  });
}
render() {
  const { count } = this.state;
  return (
    <div>
      <p>클릭 {count}</p>
      <button
       onClick={() => {
        this.setCount(count + 1);
     }}
    >
       클릭
     </button>
   </div>
  );
 )
}

export default ClassComponent;
Function 방식
import React, { useState } from 'react';

function FunctionComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
     <p>클릭 {count}</p>
     <button onClick={() => setCount(count + 1)}>
       클릭
       </button>
      </div>
  );
}

export default FunctionComponent;

Class 방식에서는 this를 통해서 state에 접근하고, Render가 필수적이었지만, Function 방식에서는 좀더 간결하고 직관적이게 상태 관리가 가능하다.

Hook에 종류

React에서 제공되는 Hook에 종류는 다음과 같다.

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImpreativeHandle
  • useLayoutEffect
  • useDebugValue
  • useDeferredValue
  • useTransition
  • useId

useState

상태 유지 값과 그 값을 갱신하는 함수를 반환하는 Hook이다.

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

형식으로 사용되며 useState에 지정된 초기 값(initialState)는 state라는 변수에 들어가며, 해당 state 내용을 바꾸고자 하면 setState를 활용한다.

setState(newState);

useEffect

useEffect(didUpdate);

어떤 effect를 발생하는 함수를 인자로 받는다.
Class 방식의 Life Cycle을 제어하는 기능을 대체한 Hook으로,
리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정하는데 사용된다.

useEffect(() => {
	console.log('렌더링 완료');
});

만약 렌더링이 아닌 마운트 되었을 경우(첫 렌더링인 경우만 실행) 실행하고자 한다면 useEfeect 두번째 파라미터로 비어있는 배열을 넣어주면 된다.

useEffect(() => {
  console.log('첫 렌더링 완료');
}, []);

마찬가지로 특정 값이 변경될 때에만 호출하고자 하는 경우는 빈 배열대신 해당 값을 넣어주면 된다.

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

useEffect(() => {
  console.log('state 변경됨');
}, [state]);

useEffect는 기본적으로 렌더링 되고 난 직후마다 실행되지만, 컴포넌트가 언마운트 되기 전이나 업데이트 직전 특정 작업을 수행하고자 한다면, return을 통해 뒷정리(cleanup) 함수를 반환하면 된다.

useEffect(() => {
  console.log('렌더링 완료');
  
  return () => {
    console.log('정리');
  }
});

만약 업데이트 직전이 아닌 언마운트인 경우에만 실행하고자 한다면 useEffect에 두 번째 파라미터에 비어있는 배열을 넣으면 된다.

useEffect(() => {
  console.log('렌더링 완료');
  
  return () => {
    console.log('언마운트 시 정리');
  }
}, []);

useContext

context 객체(React.createContext 에서 반환된 값)을 받아 그 context의 현재 값을 반환한다.

일반적으로 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 데이터를 전달하는데, 깊이가 깊어질 수록 거치는 컴포넌트가 늘어나고 코드가 반복되며 복잡성이 증가한다.
context는 전역적으로 데이터를 공유해 데이터가 필요한 컴포넌트에서 바로 사용이 되도록 돕는 객체이며, useContext는 이러한 Context를 좀더 편하게 사용하기 위한 역할이다.

Context를 이해하기 위해선 3가지 개념을 알아야한다.

  • createContext : context 객체를 생성
  • Provider : 생성한 context를 하위 컴포넌트에게 전달
  • Consumer : context의 변화를 감시하는 컴포넌트
// Context를 생성
export const MyContext = createContext();

// 다음과 같은 형태로 Context를 하위로 전달
  return (
    <>
      <MyContext.Provider value={contextValue}>
        <div>
          <Children />
        </div>
      </MyContext.Provider>
    </>
  );

// 사용하고자 하는 컴포넌트에서 useContext를 호출해 사용
const value = useContext(MyContext);

useReducer

다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트 하고자 할 때 사용하는 Hook 이다.

const [state, dispatch] = useReducer(reducer, initialArg, init);

리듀서는 현재 상태, 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태로 반환한다.

다음은 리듀서를 활용해 구현된 counter 예제이다

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

dispatch를 통해 type을 전달하고, 해당 type값은 reducer에 action으로 활용 되어 state에 대한 다양한 처리에 판단 기준이 된다.

setState와 같이 단순 상태 지정이 아닌 복잡한 상황을 고려해 다양한 state를 관리하고자 할 때 사용된다.

useCallback

메모이제이션된 콜백을 반환하는 Hook이다.

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

useCallback에 첫번째 파라미터에는 생성하고자 하는 함수를 넣고, 두 번째 파라미터에는 배열을 넣는다.
이 배열은 의존성이 성립되어 해당 요소가 변경되거나 추가되면 새로 만들어진 함수를 반환한다.

useMemo

useCallback과 비슷하지만 메모이제이션 된 값을 반환한다는 차이가 존재한다.(함수 반환, 값 반환 차이)
렌더링 과정에서 특정 값(의존성으로 설정된 값)이 변한 경우에만 연산을 실행한다.
값이 바뀌지 않았다면, 이전에 연산한 결과를 다시 사용한다.

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

useMemo로 전달된 함수는 렌더링 중에 실행되며, side effects와 같이 렌더링 중에 하지 않는 연산을 useMemo 함수내에서는 진행하면 안된다.

useRef

DOM에 접근하기 위해 ref를 사용할 때 useRef를 이용해 좀더 간편하게 사용이 가능하다.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useRef를 선언하고 특정 요소에 ref={}를 통해 useRef 변수를 지정하는 것으로 성립한다.
useRef는 current 속성을 가진 객체를 반환하는데, ref={} 형태로 지정된 useRef는 currnt 속성을 통해 해당 DOM 객체를 가리킨다.
useRef()에 기본값을 넣어주면 해당 요소가 .current에 기본값이 된다.

useImpreativeHandle

useImperativeHandle은 ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화(customizes) 한다.
useImperativeHandle는 forwardRef와 함께 사용한다.
기본 구조는 다음과 같다.

useImperativeHandle(ref, createHandle, [deps])

ref : 프로퍼티를 부여할 ref
createHandle : 객체를 리턴하는 함수. 해당 객체에 추가하고 싶은 프로퍼티를 정의

사용 예시는 다음과 같다.

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

useRef()로 선언된 inputRef는 input element와 연결된다.
Function FancyInput에 ref를 useImperativeHandle를 통해 지정하고, 내부적으로 focus에 대한 handle을 inputRef.current.focus();로 지정한다.

부모 컴포넌트에서 FancyInput = forwardRef(FancyInput);를 렌더링하면
inputRef.current.focus()의 형태로 자식 컴포넌트의 focus를 호출 할 수 있다.

useLayoutEffect

기본 구조는 useEffect와 동일하지만, 모든 DOM 변경 이후에 동기적으로 발생한다.
useEffect는 DOM의 레이아웃 배치와 페인트가 끝난 후 이펙트 함수를 호출한다.

즉 useEffect는 DOM이 화면에 그려진 이후에 호출되고, useLayoutEffect는 DOM이 화면에 그려지기 전에 호출된다.

이펙트에 처리가 길어져서 Paint -> Effect -> ReRender 사이에 갭이 큰 경우
useEffect를 사용하면 Paint한 화면을 보고있다가 갑작스레 Effect 결과가 반영되 화면이 깜박거리는 현상을 눈으로 보게 될 수 있다.

useDebugValue

useDebugValue(value)

React 개발자 도구에서 사용자 Hook 레이블을 표시하는데 사용된다.

useDeferredValue(추후에 테스트 작업 필요)

const deferredValue = useDeferredValue(value);

상태 변화의 우선순위를 지정하기 위한 Hook이다.
데이터의 전달을 지연시키는 방법을 사용하며 useMemo와 함께 종속된 값을 메모이제이션 시켜 지연시키는 동안 재 렌더링을 막는 방식으로도 사용한다.

function Typeahead() {
  const query = useSearchQuery('');
  const deferredQuery = useDeferredValue(query);

  // Memoizing tells React to only re-render when deferredQuery changes,
  // not when query changes.
  const suggestions = useMemo(() =>
    <SearchSuggestions query={deferredQuery} />,
    [deferredQuery]
  );

  return (
    <>
      <SearchInput query={query} />
      <Suspense fallback="Loading results...">
        {suggestions}
      </Suspense>
    </>
  );
}

useDeferredValue을 통해 업데이트를 지연 시킬 value를 지정한다.
useMemo를 통해 deferredQuery를 등록 해 메모제이션 시켜 지연 후 재렌더링이 필요한 시점에 업데이트가 진행되도록 한다.

useTransition(추후에 테스트 작업 필요)

const [isPending, startTransition] = useTransition();

상태 변화의 우선순위를 지정하기 위한 Hook이다.
isPending은 작업이 지연되는지 여부를 boolean 값으로 처리하며
startTransition은 낮은 우선 순위로 실행할 함수를 인자로 받는다.
예시는 다음과 같다.

function App() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);
  
  function handleClick() {
    startTransition(() => {
      setCount(c => c + 1);
    })
  }

  return (
    <div>
      {isPending && <Spinner />}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
}

button을 클릭하면 startTransition으로 감싸진 Count에 State를 변경하는 함수가 실행된다.
해당 startTansition은 낮은 우선순위로 실행되고, 실행되는 지연 시간동안은 isPending을 통해 스피너를 보여줄 수 있다.

useId

const id = useId();

서버와 클라이언트 상에서 안정적인 고유 ID를 생성하기 위한 hook이다.

function Checkbox() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input id={id} type="checkbox" name="react"/>
    </>
  );
};

커스텀 Hook

기본 제공되는 Hook 이외에도 편의에 따라서 다른 개발자들이 만든 다양한 Hook이 존재한다.
여러 컴포넌트에서 비슷한 기능을 공유하는 경우 해당 기능을 별도의 function으로 분리시키고, 호출해서 사용하는 경우 커스텀 hook으로 구분된다.

다음은 reducer를 사용해 여러가지 Input을 관리하는 기능을 Hook으로 분리시키는 예제이다.

import { useReducer } from 'react';

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value
  }
}

export default function useInputs(initialForm) {
  const [state, dispatch] = useReducer(reducer, initialForm);
  const onChange = e => {
  	dispatch(e.target);
  };
  return [state, onChange];
}

만들어진 해당 hook 다른 컴포넌트에서 호출해 사용 할 수 있다.

const [state, onChange] = useInputs({
  name: '',
  nickname: ''
});

다양한 개발자들이 자신의 hook을 npm을 통해 공유를 하고 있음으로, 주요 custom hook에 대해서는 나중에 다시 한번 정리하고자 한다.

profile
달콤살벌

0개의 댓글