Study/React | Next.js

React 깊이 이해하기(5) - React hooks

manon_e 2024. 11. 10. 23:53
반응형

 

 
 

 


 

useRef

 

useRef는 useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장할 수 있습니다.

하지만 useState와 구별되는 큰 차이점이 있습니다.

  1. useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
  2. useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.

 

렌더링에 영향을 미치지 않는 고정값을 관리하기 위해서 useRef를 사용한다면,

그냥 함수 외부에서 값을 선언하여 관리해도 되지않을까? 생각이 들 수 있습니다.

let value = 0;

const Component = ()=> {
	const handleClick = ()=> {
    	value += 1
    }
}

 

이와 같은 방식의 단점은

  1. 렌더링 되기 전부터 value라는 값이 존재하여 메모리에 불필요한 값이 생긴다.
  2. 컴포넌트의 여러 인스턴스가 동일하게 value라는 값을 가리키게 된다.

 

하나의 값을 봐야하는 상황이라면 이 방식을 사용하겠지만, 대부분의 경우에는 각 컴포넌트마다 다른 값을 바라보길 원하므로

useRef를 사용하여 위의 단점들을 해결합니다.

useRef는 컴포넌트가 렌더링 될 때만 생성되며, 컴포넌트의 인스턴스가 여러 개라도 각각 별개의 값을 바라봅니다.

 

 

각각 렌더링마다 동일한 객체를 가리키고, 값이 변경되어도 렌더링이 안된다는 점을 고려하여 useRef를 구현해보면 아래와 같습니다.

// Preact에서 제공되고있는 코드
export function useRef (initailValue) {
  return useMemo(()=> ({ current : initialValue }), [])
}

 

 

 

이러한 특징들을 활용하여 가장 일반적으로 useRef를 사용하는 예는 DOM 접근일 것입니다.

function RefComponent () {
  const inputRef = useRef()
  
  // 런더링 실행 전 (반환 전) 이므로 undefined를 반환
  console.log(inputRef.current)
  
  useEffect(()=> {
    // <input type='text' />
    console.log(inputRef.current)
  }, [inputRef])
  
  return <input ref={inputRef} type='text' />
}

 

 

 

 

또한 useState의 이전값을 저장하는 usePrevious같은 훅을 구현할 때 useRef를 유용하게 사용할 수 있습니다.

function usePrevious (value) {
 const ref = useRef()
 
 useEffect(()=> {
   // value가 변경되면 그 값을 ref에 넣어둔다.
   ref.current = value
 }, [value])
 
 return ref.current
}

function Component () {
 const [counter, setCounter] = useState(0)
 const previousCounter = usePrevious(counter)
 
 function onClick () {
    setCounter((prev)=> prev+1)
 }
 
 return (
   <button onClick={onClick}>
      {counter} {previousCounter}
   </button>
 )
}

// 0 undefined
// 1, 0
// 2, 1
...

 

 

 

 


 

 

 

useEffect

 

useEffect를 단순히 생명주기 메서드를 대체하기 위해 만들어진 훅이라고 생각할 수 있지만,

보다 정확히 정의를 하자면 useEffect의 effect는 side effect, 즉 부수효과를 의미하며,

컴포넌트가 렌더링 된 후에 어떠한 부수효과를 일으키고 싶을 때 사용하는 hook이다.

 

 

Effect는 렌더링 자체에 의해 발생하는 부수효과를 특정하는 것으로, 특정 이벤트가 아닌 렌더링에 의해 직접 발생합니다.

(렌더링 프로세스 중 commit 단계가 끝난 후에 화면 업데이트가 이루어지고 나서 실행)

 

 

기본 형태는 아래와 같습니다.

function MyComponent() {
  useEffect(() => {
    // 이곳의 코드는 *모든* 렌더링 후에 실행됩니다
  });
  return <div />;
}

 

 

여기에 의존성 배열을 추가할 수 있는데, 아무런 값도 넘겨주지 않는다면 렌더링 할 때마다 실행이 되고

빈 배열을 둔다면 비교할 의존성이 없다고 판단해 최초 렌더링 직후에 실행된 다음부터는 더 이상 실행되지 않습니다.

useEffect(() => {
  // 모든 렌더링 후에 실행됩니다
});

useEffect(() => {
  // 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);

useEffect(() => {
 // 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);

 

 

 

이때, 의존성 배열이 없는 useEffect가 매 렌더링마다 실행된다면 그냥 useEffect 없이 써도 되는 게 아닌가?

라는 생각이 들 수 있습니다. 하지만 두 코드는 명백하게 차이점이 있습니다.

// 1
function Component () {
  console.log('렌더링')
}

// 2
function Component () {
  useEffect(()=> {
    console.log('렌더링')
  })
}
  1. useEffect는 client side에서 실행되는 것을 보장해 준다.
  2. useEffect는 컴포넌트 렌더링의 부수 효과, 즉. 컴포넌트의 렌더링이 완료된 이후에 실행된다.
    위의 코드에서 1번과 같이 함수 내부에서의 직접 실행은 컴포넌트가 렌더링 되는 도중에 실행되고,
    server side rendering의 경우에 서버에서도 실행이 된다.
    이러한 작업은 함수 컴포넌트의 반환을 지연시키므로, 무거운 작업일 경우 성능을 저하시킨다.

 

 

 

useEffect 사용 시 주의할 점

  • eslint-disable-line react-hooks/exhaustive-deps 주석 자제
    컴포넌트를 마운트 하는 시점에만 무언가 하고 싶을 때 주로 사용하게 되는데
    이는 컴포넌트의 state, props와 같은 어떤 값의 변경과 useEffect의 부수 효과가 별개로 작동하게 된다는 것이므로 자제해야 한다.
  • useEffect의 첫 번째 인수에 함수명을 부여
    코드가 복잡하고 많아질수록 무슨 일을 하는 useEffect 코드인지 파악하기 어려우므로 목적을 파악하기 위해 적절한 이름을 붙여준다.
  • 거대한 useEffect 자제
    비록 useEffect는 렌더링 이후 실행되기 때문에 렌더링 작업에는 영향을 적게 미치지만, 자바스크립트 실행 성능에는 여전히 영향을 미친다. 따라서 가능한 간결하고 가볍게 유지하는 것이 좋다.
    부득이하게 크게 만들어야 한다면 적은 의존성 배열을 사용하는 여러 개의 useEffect로 분리하는 것이 좋다.
  • 불필요한 외부함수 자제
    useEffect 내에서 사용할 부수효과라면 내부에서 만들어서 정의해 사용하는 편이 좋다.

 

 

 


 

useContext

 

React는 기본적으로 부모 컴포넌트와 자식 컴포넌트로 이루어진 tree구조를 가지고 있습니다.

따라서 부모가 가지고 있는 데이터를 자식에게 넘겨주기 위해서는 props로 전달을 해주어야 합니다.

하지만 데이터를 전달받는 컴포넌트의 거리가 멀어질 수록 props drilling이 깊어져,

해당 데이터를 사용하지 않는 컴포넌트도 거쳐가야하는 번거로움이 발생합니다.

 

이러한 prop 내려주기를 극복하기 위하여 등장한 개념이 Context 입니다.

Context를 사용하면, porps의 전달없이도 선언한 하위 컴포넌트에서 자유롭게 원하는 값을 사용할 수 있습니다.

 

 

 

 

useContext의 사용법은 아래와 같습니다.

  1. Context 생성하기 - createContext
  2. Context 사용하기 - useContext
  3. Context 제공하기 - Context.Provider
import React, { createContext } from "react";
import ChildComponent from "./child-component";

export const Context = createContext(undefined);

const App = () => {
  return (
    <>
      <Context.Provider value={{ hello: "kim" }}>
        <Context.Provider value={{ hello: "Lee" }}>
          {/*  child에는 가장 가까운 provider의 'Lee'값이 제공된다. */}
          <ChildComponent />
        </Context.Provider>
      </Context.Provider>
    </>
  );
};
import { useContext } from "react";
import { Context } from "./App";

const ChildComponent = () => {
  const value = useContext(Context);

  return (
    <>
      <div>{value.hello}</div>
    </>
  );
};

export default ChildComponent

 

 

 

 

컴포넌트의 트리가 복잡해질 때 or 다수의 Provider와 useContext를 사용할 때,

해당 Context가 존재하지 않아 에러가 발생하는 것을 방지하기 위해서

별도의 함수로 감싸서 상위에 Provider가 없는 경우 에러를 사전에 발생시키는 것이 좋습니다.

 

import { createContext, PropsWithChildren, useContext } from 'react'

const Context = createContext<{ hello: string } | undefined>(undefined)

const ContextProvider = ({
  children,
  text,
}: PropsWithChildren<{ text: string }>) => {
  return <Context.Provider value={{ hello: text }}>{children}</Context.Provider>
}

const useMyContext = () => {
  const context = useContext(Context)
  if (context === undefined) {
    throw new Error('useMyContext는 ContextProvider 내부에서 사용해야 합니다.')
  }

  return context
}

const ChildComponent = () => {
  // 이 컴포넌트가 Provider 하위에 위치하지 않는다면 에러발생
  const { hello } = useMyContext()

  return <>{hello}</>
}

const ParentComponent = () => {
  return (
    <ContextProvider text="context test">
      <ChildComponent />
    </ContextProvider>
  )
}

export default ParentComponent

 

ChildComponent가 Provider 하위에 위치하지 않으면 error발생

 

 

 

 

useContext 사용 시 주의할 점

  • Component의 재활용이 어려워 진다.
    useContext가 선언되어 있으면 Provider에 의존성을 가지고 있는 것이므로 아무곳에서나 재활용하기 어려운 컴포넌트가 된다.
  • useContext를 사용하는 컴포넌트를 최대한 작게 하거나 혹은 재사용되지 않을 만한 컴포넌트에서 사용해야 한다.
  • Context가 미리는 번위는 필요한 환경에서 최대한 좁게 만들어야한다.

 

* useContext는 단순히 props 값을 하위로 전달하는 역할을 할뿐, 상태관리를 위한 API가 아니다.

  [ 상태관리 라이브러리의 조건 ]
  1. 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야한다.
  2. 필요에 따라 이러한 상태변화를 최적화할수 있어야한다.

 

 

 

반응형