[ReactHook] 리액트 내장훅 사용하기 (useContext, useRef, useMemo, useCallback, useReducer, ...)

권준혁·2020년 11월 1일
0

React

목록 보기
18/20
post-thumbnail

안녕하세요 React의 내장훅에 대한 포스팅입니다.
리액트에서 기본적으로 제공되는 내장훅들을 다양하게 활용하기 위한 목적으로 포스팅합니다.

useContext

useContext는 클래스형 컴포넌트에서 사용했던 ContextAPI를 함수형 컴포넌트에서 사용할 수 있게 해줍니다.

import React, { useContext } from "react";
const UserContext = React.createContext();
const user = {name:'jack',age:30}
export default function ParentComponent() {
    return (
        <UserContext.Provider value={user}>
            <ChildComponent></ChildComponent>
        </UserContext.Provider>
    )
}
function ChildComponent () {
    const user = useContext(UserContext);
    console.log({user})
    return (<p>...</p>)
}

클래스형 컴포넌트와 다른점은 Consumer컴포넌트 대신 useContext()를 사용한다는 것입니다.
다른 특징들은 거의 같습니다.

  • 데이터는 가장 가까운 상위 Provider의 value를 이용하게됩니다.
    • shouldComponentUpdatememoization를 사용하더라도 useContext를 사용하는 컴포넌트 자체부터 다시 렌더링됩니다.

useRef

참고 : 이전포스팅 : 클래스형 컴포넌트의 ref속성값

클래스형 컴포넌트에서는 createRef함수를 통해서 돔요소에 접근하는 방법을 사용했었습니다.
함수형 컴포넌트에서는 useRef 훅을 사용합니다.

useRef 훅의 기본형태

const refContainer = useRef(initialValue);

버튼 클릭시에 input요소에 포커스를 줍니다.

import React, { useRef } from 'react'
export default function Ref () {
    const inputEl = useRef(null);
    const onClick = () => {
        if (inputEl.current) {
            inputEl.current.focus();
        }
    }
    return (
        <div>
            <input ref={inputEl} type='text' />
            <button onClick={onClick}>Focus the text</button>
        </div>
    )
}

createRef와 마찬가지로 current속성을 이용해 돔요소에 접근할 수 있습니다.
하지만, ref속성값보다 useRef가 더 유용하다고 합니다.

리액트 문서 : Hooks API reference , useRef
이 기능은 클래스에서 인스턴스 필드를 사용하는 방법과 유사한 어떤 가변값을 유지하는 데에 편리합니다.
useRef는 순수한 자바스크립트 객체를 생성합니다. 그리고, useRef는 매번 렌더링할 때 동일한 ref객체를 제공합니다.

클래스형 컴포넌트에서는 인스턴스를 생성하기 때문에 렌더링과 무관한 값을 이용하고 싶을 때 멤버변수에 저장했었습니다.
함수형 컴포넌트는 인스턴스가 없기 때문에, 동일한 기능을 useRef훅을 이용할 수 있습니다.

useRef훅을 이용해서 이전 상탯값을 저장하는 코드입니다.

export default function Profile () {
    const [age,setAge] = useState(20);
    const prevAgeRef = useRef(20)
    useEffect (
        ()=> {
            prevAgeRef.current = age;
        },
        [age]
    )
    const prevAge = prevAgeRef.current;
    const text = age === prevAge? 'same' : age> prevAge? 'older' : 'younger';
    return (
        <div>
            <p>{`age ${age} is ${text} than age ${prevAge}`}</p>
            <button onClick={()=>{
                const age = Math.floor(Math.random()*50 +1);
                setAge(age)
            }}>나이변경</button>
        </div>

    )
}

이 컴포넌트는 age를 상탯값으로 관리하고 있습니다.
그리고 prevAgeRef는 useRef훅을 이용해 이전 상탯값인 나이를 저장합니다.
useEffect에 상탯값인 age가 변경되는 경우에 동작하도록 작성했습니다.
age가 변경되면 prevAgeRef의 current속성에 age를 저장합니다.
버튼을 클릭하면 1~50까지의 랜덤한 나이를 state에 저장합니다.

버튼을 클릭할 때마다 랜덤한 나이를 상탯값에 저장하고 이전 상탯값을 useRef를 이용해 저장하면서, 상탯값에 따라 각각 다른 텍스트를 출력합니다.

클래스형 컴포넌트에서와 다르게 함수형 컴포넌트에서 인스턴스가 없어서 발생하는 단점을 useRef를 활용할 수 있겠습니다.


useMemo

memoization은 이전값을 기억해 업데이트가 꼭 필요할 때만 해주기 때문에 성능 최적화를 할 때 사용했었습니다.

사용법

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

useEffect에서 두 번째 인수에 의존성배열을 받는 것 처럼, useMemo도 두 번째 인수에 의존성배열을 받습니다.
의존성 배열이 변경됐을 경우에만 computeExpensiveValue를 수행합니다.

주의할 점은 "렌더링 중"에 수행되기 때문에 useEffect에서 해야 하는 (side effect)를 발생시키면 안된다는 것입니다.

복잡한 계산을 useMemo로 제한하기

import React, { useMemo } from 'react'
export default function SomeCompo ({a,b}) {
    console.log({a,b})
    const value = useMemo(()=> runExpensiveCalculate(a,b),[a,b]);
    return (
        <div>
            <p>{`value is ${value}`}</p>
        </div>
    )
}
function runExpensiveCalculate (a,b) {
    return a+b;
}

runExpensiveCalculate는 굉장히 복잡한 연산이라고 생각하겠습니다.
매번 렌더링 될 때마다 이 연산을 하는 것은 성능에 부정적인 영향을 미칩니다.
useMemo를 통해 값을 관리해 성능을 향상시키는 목적으로 작성됐습니다.


useCallback

아까전에 작성했던 useRef의 예제를 보면, 렌더링 될 때마다 함수를 생성했었습니다.

사용해야 하는 경우

<button onClick={()=>{
    const profile = fetchProfile(memberId);
    setProfile(profile)
}}>프로필 가져오기</button>

이런 형태의 경우, onClick의 속성값이 렌더링시마다 계속 변경되기 때문에, PureComponent나 memo를 이용해도 불필요한 렌더링이 계속 발생합니다.
리액트 팀에서는 브라우저에서 함수 생성이 성능에 미치는 영향은 작다 라고 했다지만, useCallback을 사용하면 이런 문제를 해결할 수 있습니다.

기본 형태는 useMemo, useEffec에서 두 번째 인수로, 의존성 배열을 받는 것과 마찬가지인 형태입니다.

기본형태

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

useMemo나 useEffect useCallback의 두 번째 인수가 의존성목록을 받는 다는 점이 흥미로운 것 같습니다.


useReducer

useReducer훅은 useState의 대체함수입니다. 상탯값을 Redux패턴으로 사용할 수 있도록 해줍니다.
잠깐 Redux에 대해서 알아보면, 상탯값을 외부에서 관리하기 때문에, 상탯값을 직렬화 할 수 있습니다.

redux가 아닐 경우는 일반적으로 위 그림처럼 바로 위, 바로 아래 컴포넌트를 거쳐야 합니다.

바로 reducer훅에 대해 알아보겠습니다.

useReducer 기본형태 첫 번째

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

useReducer 기본형태 두 번째

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

두 가지 형태는 초기값을 설정하는 다른 패턴일 뿐입니다.

  • 첫 번째는 init이라는 초기값을 반환하는 함수에 initialArg를 매개변수로 넣어 초기값을 생성하는 패턴입니다.
  • 두 번째는 initObj라는 객체리터럴을 초기값으로 사용하는 패턴입니다.

공통으로 들어가있는 reducer는 기본형태를 익히는 것이 좋습니다.

reducer 기본형태

function reducer (state, action) {
    switch (action.type) {
        case 'setName' :
            return {...state, name:action.name};
        case 'setAge' : 
            return {...state, age : action.age};
        default : 
            return state;
    }
}

state는 공통상태값입니다. 기존의 전체상태값 객체입니다.
setState대신에 dispatch라는 상탯값 변경 메서드를 사용하게 될 텐데, dispatch함수의 매개변수 객체가 action이라고 생각하면 됩니다.
아래 코드를 봐주세요.

dispatch함수의 기본형태

dispatch({type:'setName', name:'kong'})

위 코드로 호출하면, action.type은 setName이 됩니다. 매개변수로 전달되는 name속성이 state에 병합되도록 작성되어 있습니다.

아래처럼 payload속성에 전달할 매개변수를 담는게 일반적입니다.

dispatch({type:'setName', payload:{name:'kong'}})

한 번 정리하겠습니다.

useReducer훅을 사용한 경우, 컴포넌트에서 이벤트를 발생시키고 싶다면 dispatch함수를 이용합니다.
dispatch함수의 매개변수 객체에 type속성에 따라 다른 동작을 할 수 있습니다.

상탯값의 name을 kong으로 변경하고 싶을 때

  • useReducer를 사용하지 않은 경우
 setState({name : 'kong}) 또는 setName('kong')
  • useReducer를 사용한 경우
 dispatch({type:'setName', name:'kong'})

그럼 컴포넌트의 이벤트속성에 dispatch함수를 작성해보겠습니다.

컴포넌트에서 useReducer훅 사용해보기

import React, { useReducer } from 'react'

const INITIAL_STATE = { name : 'empty', age : 0};
function reducer (state, action) {
    switch (action.type) {
        case 'setName' :
            return {...state, name:action.name};
        case 'setAge' : 
            return {...state, age : action.age};
        default : 
            return state;
    }
}

export default function Profile () {
    const [state, dispatch] = useReducer(reducer,INITIAL_STATE);
    return (
       <div>
          <p>{`name is ${state.name}`}</p>
          <p>{`age is ${state.age}`}</p>
          <input type='text' value={state.name}
            onChange={e=>dispatch({type:'setName', name:e.currentTarget.value})}
            />
          <input
          type='number'
          value={state.age}
          onChange={e=>dispatch({type:'setAge', age:e.currentTarget.value})}
          />
        </div>
    )
}

input요소의 onChange속성에 dispatch함수를 각각 넣었습니다.
두 개의 dispatch함수는 type이 다르기 때문에 reducer를 통해 다른 기능을 수행할 수 있습니다.

트리의 깊은곳으로 dispatch함수 전달하기

트리의 깊은곳으로 dispatch함수를 context API를 이용해 전달합니다.
이렇게 하면 상위컴포넌트에서 트리의 깊은 곳으로 이벤트 처리함수를 쉽게 전달할 수 있습니다.

Profile컴포넌트 수정

const INITIAL_STATE = {
    // ...
}
function reducer (state,action) {
    // ...
}
export const ProfileDispatch = React.createContext(null)
export default function Profile () {
    const [state, dispatch] = useReducer(reducer,INITIAL_STATE);
    const [name,age] = [state.name, state.age]
    return (
            <ProfileDispatch.Provider value={dispatch}>
                <ProfileInput name={name} age={age}/>
            </ProfileDispatch.Provider>
    )
}

컨텍스트를 이용해 dispatch함수를 전달해주는 것만으로 컴포넌트트리의 깊이에 상관없이 하위컴포넌트에서 쉽게 사용할 수 있습니다.

import React, { useContext } from 'react'
import { ProfileDispatch } from './Reducer'
export default function ProfileInput({name,age}) {
    const dispatch = useContext(ProfileDispatch)
    return (
        <div>
            <p>{`name is ${name}`}</p>
            <p>{`age is ${age}`}</p>
            <input type='text' value={name}
            onChange={e=>dispatch({type:'setName', name:e.currentTarget.value})}/>
            <input type='number' value={age}
            onChange={e=>dispatch({type:'setAge', age:e.currentTarget.value})}/>          
        </div>
    )
}

ProfileInput컴포넌트의 모든 하위컴포넌트에서 dispatch함수를 사용할 수 있습니다.
reducer에서 정의한 모든 action을 사용할 수 있기 때문에, 이벤트처리함수 하나를 ContextAPI를 이용해 전달하는 것보다 효율적입니다.


useImperativeHandle

클래스형컾모넌트에서 부모 컴포넌트는 ref객체를 이용해 자식컴포넌트의 인스턴스에 접근해 메서드를 호출하거나 state를 호출할 수도 있었습니다.
useImperativeHandle훅은 자식컴포넌트에서 부모컴포넌트에 보여질 인스턴스값을 customizes합니다. 대부분의 경우 ref를 사용한 명령형 코드는 피해야 합니다.
forwardRef로 ref값을 제대로 받아야 합니다.

먼저 useImperativeHandle을 사용하기전에 아래와 같은 컴포넌트가 있습니다.

export default function Profile (props, ref) {
    const [name, setName] = useState('');
    const [age, setAge] = useState(0);
    return (
        <div>
            <p>{`name is ${name}`}</p>
            <p>{`age is ${age}`}</p>
        </div>
    )
}

부모 컴포넌트가 Profile컴포넌트로 ref를 통해 접근 하려고 할 때, 자식컴포넌트인 Profile컴포넌트 내에서 useImperativeHandle훅을 이용해서 어떤 인스턴스값들을 노출할 지 설정하는 겁니다.

import React, { forwardRef, useState, useImperativeHandle } from 'react'
function Profile (props, ref) {
    const [name, setName] = useState('');
    const [age, setAge] = useState(0);
    useImperativeHandle(ref , ()=>({
        addAge: value=>setAge(age + value),
        getNameLength: ()=>name.length
    }))
    return (
        <div>
            <p>{`name is ${name}`}</p>
            <p>{`age is ${age}`}</p>
        </div>
    )
}
export default forwardRef(Profile)

먼저 부모컴포넌트에서 호출시에 forwardRef를 통해 ref가 잘 전달되도록 export를 작성합니다.
부모컴포넌트에서 Profile컴포넌트 호출 시 forwardRef를 경유해서 호출되므로 ref가 잘 전달됩니다.
Profle컴포넌트의 속성값의 두 번째 매개변수로 ref가 넘어옵니다.
useImperativeHandle훅으로 전달 될 값들을 정의할 수 있습니다.

대부분의 경우 ref를 사용한 명령형 코드는 피해야 하기 때문에 되도록 지양해야 하지만, 필요한 경우에는 사용할 수 있게 잘 알아둬야겠습니다.


useLayoutEffect

useLayoutEffect훅은 useEffect훅과 거의 같습니다.
다른 점은 useLayoutEffect는 동기식으로 동작하고 useEffect는 비동기식으로 동작한다는 점입니다.
기존의 클래스형컴포넌트의 componentDidMount와 componentDidUpdate는 렌더링 결과가 돔에 반영된 후 동기식으로 동작합니다.
useEffect는 비동기식으로 동작해 성능상으로 이점이 있습니다.
하지만 비동기식으로 동작하기 때문에 브라우저가 화면을 그리는중에도 동작할 수 있어, 화면의 불일치나 화면이 깨지는등의 문제가 발생한다면 useLayoutEffect훅의 사용을 고려해야합니다.
일반적으로 useEffect를 우선 사용하되, 문제 발생시에 useLayoutEffect를 고려하면 됩니다.


useDebugValue

useDebugValue는 React 개발자도구에서 커스텀 훅의 내부상태를 관찰 할 수 있습니다.

사용 예

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}

개발자 툴에서는 "FriendStatus: Online" 이런 식으로 출력됩니다.
일반적으로 작성할 필요는 없지만 커스텀훅의 세부내용을 디버깅시 보고싶을 때 상황에따라 선택적으로 작성하면 됩니다.

읽어주셔서 감사합니다!
다음 포스팅에서 다양한 커스텀훅을 작성하는 방법에 대해 포스팅하겠습니다.

React공식문서 : Hooks API Reference 와 실전리액트프로그래밍 : 이재승님지음 을 참고해 작성했습니다.

profile
웹 프론트엔드, RN앱 개발자입니다.

0개의 댓글