티스토리 뷰

함수 컴포넌트가 상태를 사용하거나 클래스 컴포넌트의 생명주기 메서드를 대체하는 등의 작업을 하기 위해 훅(hook)이 추가됨

 

3.1.1 useState

상태 정의, 상태 관리

 

리액트에서 렌더링

함수 컴포넌트의 return과 클래스 컴포넌트의 render 함수를 실행한 다음

실행 결과를 이전의 리액트 트리와 비교해 리렌더링이 필요한 부분만 업데이트

 

함수 컴포넌트에서 지역 변수로 값을 다룰 때 렌더링되지 않는 이유

: 렌더링이 발생될 때마다 함수는 새롭게 실행되고 지역 변수가 항상 초기화되기 때문

 

useState는 클로저로 구현되어 있음(함수의 실행이 종료된 이후에도 지역변수인 state를 계속 참조)

const MyReact = (function () {
  const global = {}
  let index = 0
  
  function useState(initialState) {
    if (!global.states) {
      global.states = []
    }
    
    const currentState = global.states[index] || initialState
    global.states[index] = currentState
    
    const setState = (function () {
      let currentIndex = index
      return function (value) {
        global.states[currentIndex] = value
      	// 컴포넌트 렌더링 코드 생략
      }
    })()
    
    index = index + 1    
    return [currentState, setState]
  }
  
  ...
})();

 

게으른 초기화(lazy initialization)

useState의 기본값으로 함수를 넘기는 것

state가 처음 만들어질 때만 사용되며 리렌더링이 발생된다면 함수 실행이 무시됨

- 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용

- localStorage/sessionStorage 접근, map, filter, find같은 배열 접근

 

3.1.2 useEffect

애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 매커니즘

state와 props의 변화로 일어나는 렌더링 과정에 의존성 배열의 값이 변경되면 실행되는 부수 효과 함수

 

클린업 함수의 목적

이벤트를 등록하고 지울 때 사용(이벤트 핸들러가 무한히 추가되는 것을 방지)

새로운 값(변경된 값)을 기준으로 렌더링 뒤에 실행되지만 함수가 정의됐을 당시의 값(state/props)으로 실행됨

클린업 함수를 실행하고 콜백 함수가 실행됨

→ 언마운트와 다른 개념

 

의존성 배열

빈 배열: 비교할 의존성이 없어 최초 렌더링 직후에만 실행

값을 넘기지 않는 경우: 렌더링할 때마다 실행(보통 컴포넌트가 렌더링됐는지 확인하기 위해 사용됨)

const MyReact = (function () {
  const global = {}
  let index = 0
  
  function useEffect(callback, dependencies) {
    const hooks = global.hooks
    let previousDependencies = hooks[index]
    let isDependenciesChanged = previousDependencies 
      ? dependencies.some((value, indx) => !Object.is(value, previousDependencies[idx]))
      : true
      
    if (isDependenciesChanged) {
      callback()
    }
    
    hooks[index] = dependencies
    index++
  }
  
  return { useEffect }
})();

 

useEffect 사용 시 주의할 점

1. eslint-disable-line react-hooks/exhaustive-deps 주석 자제

빈 배열일 경우 호출 위치와 콜백 함수 내부에 있는 상태의 필요성 점검

빈 배열이 아닐 경우 호출 위치 점검, 호출을 원하지 않는 값을 메모이제이션

2. useEffect 첫 번째 인수에 함수명 부여(기명 함수)

useEffect의 목적 파악

3. 거대한 useEffect를 만들지 말 것

부수 효과가 클수록 애플리케이션 성능에 악영향

적은 의존성 배열을 사용하는 여러 useEffect로 분리

4. 불필요한 외부 함수를 만들지 말 것

외부 함수를 위한 불필요한 코드가 많아짐(메모)

 

useEffect 콜백으로 비동기 함수를 넣을 수 없는 이유

useEffect의 경쟁 상태(race condition) 문제

state가 변경됨에 따라 이전 비동기 함수의 결과가 나중에 노출될 수 있음

 

shouldIgnore 변수를 사용하거나 AbortController 활용하여 요청 자체를 취소

useEffect(() => {
  let shouldIgnore = false
  
  async function fetchData() {
    const response = await fetch('...')
    const result = await response.json()
    if (!shouldIgnore) {
      setData(result)
    }
  }
  
  fetchData()
  
  return () => {
    shouldIgnore = true 
  }
}, [])

 

3.1.3 useMemo

비용이 큰 연산에 대한 결과를 저장해두고 저장된 값을 반환하는 훅(최적화)

컴포넌트도 반환할 수 있으나 memo가 컴포넌트를 메모이제이션하기 위해 설계되었기 때문에 컴포넌트는 memo 사용하는 것이 좋음(목적, 최적화)

인수: 값을 반환하는 생성 함수, 의존성 배열

 

3.1.4 useCallback

함수를 메모이제이션

useMemo로 useCallback을 구현할 수 있으나 코드가 길어지고 복잡하여(함수를 반환하는 함수) 리액트에서 따로 제공하는 것으로 추측

인수: 함수, 의존성 배열

 

3.1.5 useRef

렌더링을 발생시키지 않고 원하는 상태값을 저장

사용 예시: DOM에 접근

useRef의 최초 기본값은 useRef 실행 시 넘겨받은 인수

 

useState와 차이

- 객체 반환, current key로 값에 접근하고 변경할 수 있음

- 값이 변경되더라도 렌더링이 발생하지 않음

 

useRef 대신 컴포넌트 외부에 값을 선언한다면

- 컴포넌트가 렌더링되지 않아도 메모리를 차지하고 있음

- 인스턴스가 모두 같은 값을 가질 때만 유효

function usePrevious(value) {
  const ref = useRef()
  
  useEffect(() => {
    ref.current = value
  }, [value])
  
  return ref.current
}
// Preact의 useRef 구현
function useRef(initialValue) {
  return useMemo(() => ({ current: initialValue }), [])
}

 

3.1.6 useContext

상위 컴포넌트에서 만들어진 Context를 하위 컴포넌트에서 사용할 수 있도록 만들어진 훅

 

Context

props drilling을 극복하기 위해 등장한 개념

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

function ParentComponent() {
  return (
    <Context.Provider value={{ someKey: 'someValue'}}>
      <ChildComponent />
    </Context.Provider>
  )
}

function ChildComponent() {
  const value = useContext(Context)
  return value ? value.someKey : ''
}

 

컴포넌트 트리가 복잡해질수록 콘텍스트를 사용하는 게 어려워짐(콘텍스트가 존재하지 않아 에러가 발생하는 경우)

→ useContext를 커스텀 훅으로 만들어 에러 체크

function useMyContext() {
  const context = useContext(MyContext)
  
  if (context === undefined) {
    throw new Error('useMyContext는 ContextProvider 내부에서만 사용할 수 있습니다.')
  }
  return context
}

 

useContext 사용 시 주의할 점

Provider에 의존성을 가지게 되어 컴포넌트 재활용이 어려워짐

컨텍스트 범위를 적절히 좁히지 않는다면 필요치 않은 곳에서 접근할 수 있는 등 리소스 낭비 발생

 

컨텍스트와 useContext는 상태를 주입해 주는 API(렌더링 최적화되지 않음)

상태 관리 라이브러리는 상태를 기반으로 다른 상태를 만들 수 있고, 상태 변화를 최적화할 수 있어야 함

 

3.1.7 useReducer

useState의 심화. state 변경을 제한하는 것이 목적

state를 사용하는 로직과 비즈니스 로직을 분리하여 state 관리가 쉬워짐

반환: 길이가 2인 배열. state, dispatcher(action을 전달)

인수: reducer(action 정의), initialState(초깃값), init(optional. 게으른 초기화를 위한 함수로 initialState를 인수로 실행됨)

 

3.1.8 useImperativeHandle

forwardRef

ref가 예약어라서 props.ref를 사용하지 못함

컴포넌트를 forwardRef로 감싸서 사용하며 ref를 전달하는 데 일관성 제공

 

부모에게서 받은 ref에 값이나 액션을 추가로 정의할 수 있는 훅

const Temp = forwardRef((props, ref) => {
  useImperativeHandle(
    ref, // 전달받은 ref
    () => ({ alert: () => alert(props.value) }), // 추가할 동작
    [props.value] // 의존성 배열
  )
  return <input ref={ref} {...props} />
})

// 사용
ref.current.alert()

 

3.1.9 useLayoutEffect

useEffect와 비슷. 모든 DOM의 변경(렌더링) 후 동기적으로 발생

리액트가 DOM 업데이트 → useLayoutEffect → 브라우저에 변경 사항 반영 → useEffect 실행

 

useLayoutEffect의 실행이 완료되어야 화면이 그려지기 때문에 멈춤 현상 등 성능에 문제가 발생할 수 있음

 

DOM은 계산됐지만 화면이 변경되기 전에 하고 싶은 작업이 있는 경우에 사용

ex. DOM 요소 기반 애니메이션, 스크롤 위치 제어

 

3.1.10 useDebugValue

개발 과정에서 사용, 다른 훅 내부에서만 실행 가능

인수: 값, 포매팅 함수

 

3.1.11 훅의 규칙

최상위에서만 호출해야 한다

조건문, 중첩 함수 X

컴포넌트가 렌더링될 때마다 훅이 항상 동일한 순서로 호출되기를 보장하기 위함

파이버 객체가 순서에 의존하여 값을 저장하고 이전 값과 비교

 

리액트 함수 컴포넌트, 사용자 정의 훅에서만 훅을 호출할 수 있다

 

3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?

3.2.1 사용자 정의 훅(customHook)

리액트 훅 기반

use prefix 규칙 

 

3.2.2 고차 컴포넌트(HOC. Higher Order Component)

고차 함수(Higher Order Function. 함수를 인수로 받거나 결과로 반환하는 함수)의 일종

with prefix 규칙

부수효과를 최소화 → 인수로 받는 컴포넌트의 props를 임의로 수정/추가/삭제하지 말아야 함

고차 컴포넌트가 중첩될 경우 복잡성이 커지기 때문에 고차 컴포넌트를 최소한으로 사용

 

React.memo

props의 변화가 없을 때 컴포넌트의 렌더링을 방지하기 위해 만들어진 고차 컴포넌트

 

interface LoginProps {
  loginRequired?: boolean
}

function withLoginComponent<T>(Component: ComponentType<T>) {
  return function (props: T & LoginProps) {
    const { loginRequired, ...restProps } = props
    
    if (loginRequired) {
      return '로그인이 필요합니다.'
    }
    return <Component {...(restProps as T)} />
  }
}

const Component = withLoginComponent((props: {value: string}) => {
  return <h3>{props.value}</h3>
})

function App() {
  return <Component value="text" loginRequired={isLogin} />
}

 

3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?

로직을 공통화하여 관리할 수 있다는 특징

 

사용자 정의 훅이 필요한 경우

리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있는 경우

 

고차 컴포넌트를 사용해야하는 경우

비로그인, 에러 발생한 경우 공통 컴포넌트를 노출하기 위해 사용

댓글
공지사항