모던 리액트 3장

모던 리액트 공부 기록

2024-01-16

마크다운 이미지

훅은 어디서 오는거지?

우리가 쓰는 react의 여러 훅들은 사실 ReactHooks.js이라는 파일에서 가져온다.

ReactHooks.js

/src/React.js
import {
  createElement as createElementProd,
  createFactory as createFactoryProd,
  cloneElement as cloneElementProd,
  isValidElement,
} from './ReactElement';
import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import {forwardRef} from './ReactForwardRef';
import {memo} from './ReactMemo';
import {cache} from './ReactCache';
import {postpone} from './ReactPostpone';
import {
  getCacheSignal,
  getCacheForType,
  useCallback,
  useContext,
  useEffect,
  useEffectEvent,
  useImperativeHandle,
  useDebugValue,
  useInsertionEffect,
  useLayoutEffect,
  useMemo,
  useSyncExternalStore,
  useReducer,
  useRef,
  useState,
  useTransition,
  useDeferredValue,
  useId,
  useCacheRefresh,
  use,
  useMemoCache,
  useOptimistic,
} from './ReactHooks';

그럼 이제 ReactHook.js를 까보자. 여러 훅들이 정의되어 있지만 가장 간단한 훅인 useState의 구현체를 살펴보자. dispatcher를 선언하고 resolveDispatcher라는 함수를 할당한다.

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

그럼 다시 resolveDispatcher를 까보자. 이 함수는 다음과 같이 정의되어 있다. 이 함수는 다시 ReactCurrentDispatcher를 가져온다.

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
          ' one of the following reasons:\n' +
          '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
          '2. You might be breaking the Rules of Hooks\n' +
          '3. You might have more than one copy of React in the same app\n' +
          'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return ((dispatcher: any): Dispatcher);
}

ReactCurrentDispatcher함수는 다음과 같이 정의되어 있다. 그냥 객체 하나가 있고 current라는 필드가 있다.

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';

/**
 * Keeps track of the current dispatcher.
 */
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

훅 객체는 외부 -> 내부에서 ReactCurrentDispatcher.current을 통해 주입받는다. 그리고 이 외부 -> 내부에서 의존성을 주입할 때 한단계를 더 거치게 되는데 ReactSharedInternal.jsshared패키지가 이 역할을 한다.

그리고 reconciler패키지가 훅 객체를 주입한다.

shared패키지와 ReactSharedInternal.js

먼저 ReactSharedInternal.js를 까보자.(Internal Server와 Client로 나누어져있는데 Client를 보겠다!)

이 파일은 외부에서 주입받길 기다리는 모듈들의 대기소이다. (ReactCurrentDispatcher도 훅을 이곳에서 주입받는다.)

//ReactSharedInternal.js
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import ReactCurrentDispatcher from './ReactCurrentDispatcher';
import ReactCurrentCache from './ReactCurrentCache';
import ReactCurrentBatchConfig from './ReactCurrentBatchConfig';
import ReactCurrentActQueue from './ReactCurrentActQueue';
import ReactCurrentOwner from './ReactCurrentOwner';
import ReactDebugCurrentFrame from './ReactDebugCurrentFrame';
import {enableServerContext} from 'shared/ReactFeatureFlags';
import {ContextRegistry} from './ReactServerContextRegistry';

const ReactSharedInternals = {
  //현재 활성화된 훅 디스패처
  ReactCurrentDispatcher,
  ReactCurrentCache,
  ReactCurrentBatchConfig,
  ReactCurrentOwner,
};

if (__DEV__) {
  ReactSharedInternals.ReactDebugCurrentFrame = ReactDebugCurrentFrame;
  ReactSharedInternals.ReactCurrentActQueue = ReactCurrentActQueue;
}

if (enableServerContext) {
  ReactSharedInternals.ContextRegistry = ContextRegistry;
}

export default ReactSharedInternals;

shared는 말 그대로 모든 패키지가 공유하는 폴더이다. 이 곳에서도 ReactSharedInternals.js 파일을 찾을 수 있다.

shared -> ReactSharedInternals.js
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import * as React from 'react';

const ReactSharedInternals =
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

export default ReactSharedInternals;

reconciler -> shared패키지의 ReactSharedInternal -> React코어의 ReactSharedInternal -> ReactCurrentDispatcher -> ReactHooks -> 훅

useState훅

useState는 함수형 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅이다.

import {useState} from 'react'

//초기값을 넘겨주지 않으면 undefined
const [state,setState] = useState(initalState)

만약 useState를 사용하지 않고 함수 내부에서 상태를 관리한다면 어떻게 될까?

function Component() {
    let state = 'hello'

    function handleClickButton() {
        state = 'hi'
    }

    return (
        <>
          <h1>{state}</h1>
          <button onClick = {handleClickButton}>hi</button>
        </>
    )
}

리액트에서 렌더링은 함수형 컴포넌트의 return문과 클래스형 컴포넌트의 render함수를 실행한 후 이 실행 결과를 이전 트리와 비교해 리렌더링이 필요한 부분을 찾아서 발생시킨다. 리렌더링을 일으키는 요소 중에는 크게 아래와 같았다. 다시말해, 위 코드는 리렌더링을 일으키는 어떤 조건에 전혀 해당되지 않는다.

그럼 아래와 같이 바꾸면 어떨까?

function Component() {
    const [,triggerRender] = useState()
    let state = 'hello'

    function handleButtonClick() {
        state = 'hi'
        triggerRender()
    }

    return (
        <>  
          <h1>{state}</h1>
          <button onClick = {handleButtonClick}>hi</button>
        </>
    )
}

위 경우 버튼을 클릭하면 렌더링이 일어난다. 그러나 상태가 갱신되지 않는데, 함수형 컴포넌트의 결과인 return의 값을 비교해 렌더링을 실행한다. 매번 렌더링이 일어날 떄마다, 저 Component가 다시 만들어지고 결국 새로운 함수에서 state는 hello로 매번 초기화되므로 상태가 변경되지 않는다.

그렇다면 useState의 결과는 어떻게 함수가 실행되어도 그 값을 갖고 있을까? useState훅을 다음과 같이 만들어보자.

function useState(initalState) {
    let initalState = initalState

    function setState(newState) {
        initalState = newState
    }

    return [initalState,setState]
}

이 코드는 정상적으로 동작하지 않는다. 구조분해할당으로 이미 initalState의 값을 결정한 상태이기 때문에, setState의 호출에도 불구하고 최신의 상태를 가져오지 못한다. 이를 해결하려면 setState를 함수로 바꿔서 state의 값을 반환하게 만들면 된다.

function useState(initalState) {
    let initalState = initalState

    function state() {
        return initalState
    }

    function setState(newState) {
        initalState = newState
    }

    return [initalState,setState]
}

const [state,setState] = useState(0)
setState(1)

console.log(state())

다만 실제 react에서는 값을 얻기 위해 함수를 사용하지 않는데, 이를 위해 react는 클로저를 사용한다.

const MyReact = function() {
    const global = {}
    let index = 0

    function useState(initalState) {
        //애플리케이션의 전체 state관리용
        if(!global.states) {
            global.states = []
        }

        //states 정보 조회 -> 현재 상태 값이 없다면 초기 값으로
        const currentState = global.states[index] ?? initalState
        //states의 값을 갱신
        global.states[index] = currentState

        //setter함수
        const setState = (function() {
            //클로저를 통해 즉시 실행 함수의 문맥으로 index가둔다. index를 계속 참조한다.
            let currentIndex = index
            return function(value) {
                global.states[currentIndex] = value
            }
        }())

        //하나의 state마다 index를 할당하고 그 index가 global.states를 가리킨다.
        index = index + 1

        return [currentState,setState]
    }

}

useState는 자바스크립트의 클로저에 의존해 구현된 것을 짐작할 수 있다. 클로저를 사용함으로써 외부에 값을 노출시키지 않고 컴포넌트가 렌더링되어도, useState에서 이전 값을 정확히 알 수 있다.

useState의 인수로 특정한 값을 넘기는 함수를 인수로 넣어줄 수 있다.

// 함수를 실행해 값을 반환한다.
const [count,setCount] = useState(() => 
    Number.parseInt(window.localStorage.getItem(cacheKey))
)

게으른 초기화 함수는 오직 state가 처음 만들어질 때 실행된다. 이후 다시 리렌더링 된다면, 이 함수의 실행은 무시된다.


//매번 리렌더링 될떄마다(setState가 호출될때마다 localStorage를 읽는다.)
const Counter = () => {
    const initalState = Number.parseInt(window.localStorage.getItem(key))
    const [count,setCount] = useState(initalState)
}


//딱 초기화 할 때 한번만 호출된다.

const Counter = () => {
    const [count,setCount] = useState(() => 0)
}

이러한 방법은 useState의 초기값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하면 좋다.

(localStorage나 sessionStorage에 대한 접근, map,filter등의 배열에 대한 접근 등)

useEffect

관련 글

useEffect의 정의를 정확하게 내리면, useEffect는 애플리케이션 내 컴포넌트의 여러 값을 활용해 동기적으로 부수효과를 만드는 방법이다. 그리고 이 부수효과는 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요하다. useEffect의 의존성 배열이 바뀔 때마다 첫번째 콜백이 실행된다.

그러면 어떻게 의존성 배열이 변경된 것을 알 수 있을까? 여기서 함수형 컴포넌트는 매번 함수를 실행해 렌더링을 수행한다는 점을 알아두자!

function Component() {
    const [count,setCount] = useState(0)

    useEffect(() => {
        console.log(count)   
    })
    function handleClick() {
        setCount((prev) => prev + 1)
    }

    return (
        <>
          <h1>{count}</h1>
          <button onClick = {handleClcik}>++</button>
        </>
    )
}

useEffect는 자바스크립트의 proxy, 옵저버 패턴 등과 같은 기능을 써서 변화를 감지하는 것이 아닌,

렌더링을 할 때마다 의존성에 있는 값을 보면서, 이 의존성의 값이 이전과 다른지 확인하고 다르다면, 부수효과의 함수를 실행하는 함수이다. 그러면 클린업 함수는 대체 어떤 역할을 할까?

일반적으로 이벤트를 등록하고 지울 때 사용해야 한다고 알려져 있다.

import {useState,useEffect} from 'react'

export default function App() {
    const [counter, setCounter] = useState(0)

    function handleClick() {
        setCounter((prev) => prev + 1)
    }

    useEffect(() => {
        function addMouseEvent() {
            console.log(counter)
        }  
        
        window.addEventListener('click',addMouseEvent)

        //클린업 함수
        return () => {
            console.log('클린업 함수 실행!',counter)
            window.removeEventListener('click',addMouseEvent)
        }
    },[counter])

    return (
        <>
          <h1>{counter}</h1>
          <button onClick = {handleClick}>++</button>
        </>
    )
}

//클린업 함수 실행 ! 0
//1

//클린업 함수 실행 ! 1
//2

이 과정을 직관적으로 코드로 보여주면 다음과 같다. 렌더링이 발생할 때마다 count가 어떤 값으로 선언되어있는지 보여준다.

useEffect(() => {
    function addMouseEvent() {
        console.log(0)
    }

    window.addEventListener('click',addMouseEvent)

    //클린업 함수
    //다음 렌더링이 끝나고 실행된다.

    return () => {
        console.log(0)
        window.removeEventListener('click',addMouseEvent)
    }
},[count])

//그 이후 실행
useEffect(() => {
    function addMouseEvent() {
        console.log(1)
    }

    window.addEventListener('click',addMouseEvent)

    //클린업 함수
    //다음 렌더링이 끝나고 실행된다.

    return () => {
        console.log(1)
        window.removeEventListener('click',addMouseEvent)
    }
},[count])

결국 useEffect안의 콜백이 존재한다면, 이전의 클린업 함수를 반드시 실행하게 된다. 만약 이벤트를 걸어주는 콜백을 달고 클린업 함수를 반환하지 않았다고 생각해보자.

콜백이 실행될 떄마다 매번 이벤트가 달아지고, 이 이벤트는 제거되지 않는 무한 이벤트 추가와 같은 끔찍한 일이 벌어질 수 있다. 클린업 함수는 함수형 컴포넌트가 리렌더링 되었을 때, 의존성 변화가 있었을 당시 값을 기준으로 실행된다!.!

의존성 배열

의존성 배열은 보통 빈 배열을 두거나, 아예 아무런 값도 넘기지 않거나, 혹은 사용자가 직접 원하는 값을 넣어줄 수 있다. 만약 빈 배열을 두면, 최초 렌더링 이후 더 이상 실행되지 않고 아무런 값도 넘겨주지 않는다면 렌더링 될때마다 실행된다. (보통 컴포넌트가 렌더링 되었는지 확인할 때 사용할 수 있다.)

useEffect(() => {
    console.log('컴포넌트 렌더링됨!')   
})

두 코드의 차이점을 살펴보자

function Component() {
  console.log('foo')
}

function Component() {
  useEffect(() => {
    console.log('bar')
  })
}

의존성 배열의 이전값과 현재 값의 얕은 비교(Object.is)로 구현되어 있다.

이전 의존성 배열과 현재 의존성 배열의 값에 변경사항이 있으면 callback으로 선언한 부수효과를 실행한다.

useEffect를 사용할 때 주의할 점

(개인적으로 정말 궁금했던 점)

린트의 규칙은 최대한 살리면서 개발하자.

대부분 빈 배열을 의존성으로 넣어줄 때, 즉 컴포넌트를 마운트 하는 어떤 시점에 무언가를 하고 싶다는 의도로 작성한다.(정작 나도 많이..) 그러나 이는 클래스형 컴포넌트의 componentDidMount에 기반한 접근법으로 가급적 사용하면 안된다.

useEffect는 반드시 의존성 배열로 전달한 값의 변경에 따라 실행해야 하는 훅이다.

그러나 의존성 배열을 넘기지 않은 채 콜백함수에 특정 값을 사용한다는 것은, 이 부수 효과가 실제 변화를 관찰하고, 실행해야 하는 값과 별개로 동작해야 한다는 것을 의미한다. 즉 컴포넌트의 state,props의 변경과 useEffect의 부수 효과가 별개로 동작하게 된다!

function Component({log} : {log:string}) {
    useEffect(() => {
        logging(log)
    },[])
}

이렇게 컴포넌트가 최초 마운트 되었을 때 로깅을 남기는 용도로 코드를 작성했다고 가정해보자.

그러나 당장 문제가 없더라도, 버그의 위험성을 안고 있다..! log가 아무리 변하더라도, useEffect의 부수효과는 실행되지 않는다.

useEffect를 비동기 함수로 사용하는 경우, race-condition 문제가 발생할 수 있다. 만약 비동기 함수를 사용한다면 클린업 함수에 이전 비동기 함수에 대한 처리를 추가하는 것이 좋다. (클린업함수의 실행 순서를 보장할 수 없다)

가능한 한 useEffect는 간결하고 가볍게 유지하는 것이 좋다

useMemo

useMemo는 비용이 큰 연산의 결과를 저장(메모리제이션)해두고 이 저장된 값을 반환하는 훅이다.

첫번쨰 인자로 어떤 값을 반환하는 생성함수로, 두번째 인자로는 해당 함수가 의존하는 값의 배열을 전달한다.

useMemo는 의존성 배열의 값이 변경되지 않았다면 이전에 기억해 둔 값을 반환하고, 변경되었다면 첫번째 함수를 실행하고 그 값을 반환하고 기억한다.(컴포넌트 또한 useMemo로 메모리제이션 해둘 수 있다.)

//컴포넌트의 props를 기준으로 컴포넌트 자체를 기억해버린다!

function ExpensiveComponent({value}) {
    useEffect(() => {
        console.log('렌더링')
    })
    return <span>{value}</span>
}

function App() {
 const memoComponent = useMemo(() => <ExpensiveComponent value = {value} />,[value])   

 return (
    <div>
        {memoComponent}
    </div>
 )
}

"비용이 많이 드는 연산"이라면 useMemo를 사용할 수 있다!

결론 : 비용이 큰 연산에 대한 결과를 메모이제이션하고 저장된 값을 반환하는 훅

useCallback

useMemo가 값을 기억한다면, useCallback은 인수로 넘겨받은 콜백 자체를 기억한다. 즉, 특정 함수를 새로 만들지 않고 재사용하게 된다.

const ChildComponent = memo(({ name , value , onChange})) => {
    useEffect(() => {
        console.log('렌더링!',name)
    })

    return (
        <>
          <h1>{name} {value ? '켜짐' ? '꺼짐'}</h1>
          <button onClick = {onChange}>toggle</button>
        </>
    )
}

function App() {
    const [state1,setState1] = useState(false)
    const [state2,setState2] = useState(false)

    const toggle1 = () => {
        setState1(!state1)
    }

    const toggle2 = () => {
        setState2(!state2)
    }

    return (
        <>
          <ChildComponent name = "1" value = {state1} onChange = {toggle1} />
          <ChildComponent name = "2" value = {state2} onChange = {toggle2} />
        </>
    )
}

memo를 이용해 컴포넌트를 메모리제이션해두었지만, App의 자식 전체가 렌더링되고 있다. ChildComponent의 memo를 씌우면 name,value,onChange의 값을 모두 기억하고 , 이 값들이 변경되지 않는 한 다시 렌더링 되지 않는다.

그러나 어느 한 버튼을 누르게 된다면 -> 이 버튼이 setState을 호출하고 -> App컴포넌트가 다시 렌더링되고 onChange함수가 새로 다시 만들어진다. 따라서 의도한 대로 동작하지 않게 된다.

//상태값이 변경될 때만 함수가 재생성되고, 그 외에는 이전에 메모리에 저장한 함수를 재사용

//ChildComponent는 자신에게 전달된 onChange 함수가 변경되지 않는 한 불필요한 렌더링을 방지하게 된다.

function App() {
    const [state1,setState1] = useState(false)
    const [state2,setState2] = useState(false)

    const toggle1 = useCallback(() => {
        setState1(!state1)
    }, [state1])

    const toggle2 = useCallback(() => {
        setState2(!state2)
    }, [state2])

    return (
        <>
          <ChildComponent name = "1" value = {state1} onChange = {toggle1} />
          <ChildComponent name = "2" value = {state2} onChange = {toggle2} />
        </>
    )
}

useRef

useState와 동일하게 컴포넌트 내부의 렌더링이 발생해도 변경 가능한 상태값을 지닌다. 그러나 useState와 두가지의 차이가 있다.

렌더링에 영향을 미치지 않으면 그냥 함수 외부에서 값을 선언하고 관리하는 게 좋지 않을까?

 let value = 0
  function Component() {
    return <>{value}</>
  }

이 방식은 크게 다음과 같은 단점이 있다.

useRef는 이 두가지 단점을 해결한다. 컴포넌트가 렌더링 될떄만 생성되고 무조건 별개의 값을 바라본다.

Preact는 useRef을 useMemo로 구현한다. 렌더링에 영향을 미치면 안되기 떄문에 useMemo에 빈 배열을 두고, 각 렌더링마다 동일한 객체를 바라보게 된다.

자바스크립트의 특징, 객체의 값을 변경해도 객체를 가리키는 주소가 변경되지 않는다는 것을 떠올리면 useMemo로 useRef를 구현할 수 있다

export function useRef(initalValue) {
    currentHook = 5
    return useMemo(() => {current : initalValue} , [])
}

useContext

리액트 애플리케이션은 부모컴포넌트와 자식 컴포넌트로 이루어진 트리 구조를 갖기 떄문에 부모의 데이터를 사용하고 싶다면 props로 데이터를 넘겨준다. 그러나 전달해야하는 부모-자식의 깊이가 깊어지면 props drilling 현상이 발생한다. 콘텍스트를 사용하면 명시적인 props 전달 없이도 하위 컴포넌트 전부에서 원하는 값을 자유롭게 쓸 수 있다.

마크다운 이미지

useContext를 사용하면 상위 컴포넌트 어딘가에 선언된 <Context.Provider>의 값을 가져온다. useContext 내부에서 해당 콘텍스트가 존재하는 환경인지 , 초기화 되어 값을 내려주는지 확인하는 것이 좋다.

function useMyContext() {
    const context = useContext(myContext)
    if(context === undefined){
        throw new Error(
            'Context Error!'
        )
    }
}

useContext를 함수형 컴포넌트에서 쓰면 컴포넌트의 재활용이 어려워진다는 점을 염두에 두자!

useContext가 선언되어있으면 Provider와 강한 의존성을 갖게 된다.

이러한 상황을 막으려면, useContext를 사용하는 컴포넌트를 최대한 작게하거나, 재사용되지 않을 컴포넌트에만 사용해야 한다. 콘텍스트와 useContext는 상태 관리를 위한 리액트의 API가 아닌 상태를 주입하는 API이다.

일반적인 상태 관리 라이브러리는 다음을 만족한다.그러나 콘텍스트는 이 둘 중 아무것도 하지 못한다.

상태가 변화하면 프로바이더 트리 전체가 리렌더링된다 . 물론 React.memo를 사용해 최적화할 수 있다.

useReducer

마크다운 이미지

useState와 비슷하지만 좀 더 복잡한 상태값을 미리 정해둔 시나리오에 따라 관리할 수 있다. 반환값은 useState와 동일하게 길이가 2인 배열이다.

setState의 내부 로직이 복잡해지면 복잡해질수록, 컴포넌트가 읽기 힘들어지고, 상태 관리에 어려움을 겪을 수 있다. (이전 상태를 참조하여서 무언가 복잡한 일을 해야 할 경우 등)

setShoppingCart((prevShoppingCart) => {
    const updatedItems = [...prevShoppingCard.items];

    const existingCartItemIndex = updatedItems.findIndex((cardItem) => cartItem.id === id)

    if(existingCardItem){
        ...// 복잡한 
    }
    else{
        ...//
    }
})

이를 위해 리액트의 또다른 상태 관리 훅인 useReducer를 쓸 수 있다.

Reducer는 복잡한 값을 더 단순한 형태로 만드는 함수를 의미한다. 예를 들어 다음의 배열 [5,10,100]을 더 단순한 숫자(모두 더한 숫자) 115로 만드는 것이 reducer의 역할이다.

반환값

const [state,dispatch] = useReducer(StateReducer);

인수로 넘어가는 값

//리듀서 함수는 2개의 인수를 받는다. (상태와 액션)
function StateReducer(state, action) {
    //업데이트 된 상태를 반환한다.
    return state
}

리듀서 예시

이렇게 useReducer를 사용하면 state를 사용하는 로직과 이를 관리하는 로직의 분리가 가능하여 state를 관리하기 한결 쉬워진다. Preact의 useState는 useReducer로 구현되어 있다.

export function useState(initalState) {
    currentHook = 1
    return useReducer(invokeOrReturn,initalState)
}

첫번쨰 인수는 값을 업데이틓하는 함수여야 값 그 자체여야 한다.

function reducer(prevState,newState) {
    return typeof newState === 'function' ? newState(prevState) : newState
} 

두번쨰 값은 별다른 처리가 없고, 세번째 인수는 두번째 값을 기준으로 게으른 초기화를 한다.

function init(initArg: Initalizer) {
    return typeof initArg === 'function' ? initArg() : initArg
}

반대로 useReducer를 useState도 구현할 수도 있다. 결국 클로저를 사용해 값을 가둬서 관리하는 것은 useState나 useReducer나 동일하다.

useImperativeHandle

forwardRef는 useRef에서 반환하는 객체로, 리액트의 props인 ref를 넣어 HTMLElement에 접근하는 용도로 사용된다. 즉 상위 컴포넌트에서 접근하고 싶은 ref가 있지만 이를 직접 props로 넣어 사용할 수 없으면 어떻게 해야 할까?

마크다운 이미지

fowardRef가 등장한 배경으로는 ref 전달 시 일관성을 제공하기 위해서이다.

const ChildComponent = forwardRef((props,ref) => {
    useEffect(() => {
      console.log(ref)
    },[ref])

    return <div>안녕</div>
})

const ParentComponent = () => {
    const inputRef = useRef()

    return (
    <input ref = {inputRef} />
    <ChildComponent ref = {inputRef} />
    
    )
}

ref를받고자하는 컴포넌트를 forwardRef로 감싸고 두번쨰 인수로 ref를 전달받는다. 이제 부모에서 자식으로 ref를 넘겨주면 된다. useImperativeHandle 훅은 부모에서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅이다.

React는 선언형 컴포넌트를 지향한다. 상태의 변화에 따라 사전에 정의한 결과를 보여주는 것을 선언형 컴포넌트라고 이해한다면, 명령형 함수는 이와 대척점에 있다고 할 수 있다. useImperativeHandle은 이름 그대로 명령형 함수를 사용할 수 있는 Hook을 의미한다. 따라서, React에서는 이런 명령형 훅은 자주 사용하는 것을 권장하지 않는다. 그럼에도 useImperativeHandle와 같은 Hook을 만들게 된 이유가 있다. props를 통해서 선언형 데이터만으로는 자식 컴포넌트의 동작을 구현하기 어려운 경우가 있기 때문이다. 여기서 어렵다고 표현한 이유는, 대부분의 경우 props와 useEffect 등을 통해서 불편하지만 의도한 동작을 만들 수는 있기 때문이다. 그러나 명령형 함수를 통해서 이런 구현이 훨씬 간단해질 수 있다.

useLayoutEffect

이 훅은 useEffect와 훅의 형태나 사용 예제가 동일하다. 보통 브라우저 페인트 전 DOM 조작, 혹은 레이아웃 정보를 읽어야 할 때 사용한다.(아직 한번도 쓴 적이 없긴 하다..)

function App(){
    const [count,setCount] = useState(0)

    useEffect(() => {
        console.log('useEffect' + count)
    },[count])

    useLayoutEffect(() => {
        console.log('useLayoutEffect' + count)
    },[count])

    function handleClick() {
        setCount((prev) => prev + 1)
    }

    return (
        <>
          <h1>{count}</h1>
          <button onClick = {handleClick}>+</button>
        </>
    )
}

이 훅에서 중요한 부분은 모든 DOM의 변경 후에 useLayoutEffect의 콜백이 동기적으로 실행된다는 점이다.

마크다운 이미지

브라우저의 변경 사항 전 실행 : useLayoutEffect훅

훅의 규칙

훅은 최상단에서만 호출해야 하고, 반복문, 조건문 등에서 훅을 호출할 수 없다. 사용자 정의 훅, 리액트 함수형 컴포넌트에서만 훅을 쓸 수 있다. 훅에 대한 정보는 리액트 어딘가의 index와 같은 key를 기준으로 구현되어 있다. 또한 순서에 큰 영향을 받는다.

리액트 훅은 파이버 객체의 링크드 리스트의 호출 순서에 따라 저장된다 각 훅이 파이버 객체 내에서 순서에 의존해 state나 effect의 결과에 대한 값을 저장하고 있기 때문이다.

function Component() {
    const [count,setCount] = useState(0)
    const [required,setRequired] = useState(false)

    useEffect(() => {

    },[count,required])
}

이 컴포넌트는 다음과 같은 형태로 저장된다.

{
    memorizedState:0,
    baseState:0,
    queue:{..},
    next:{ //setRequired훅
        memorizedState:false,
        next:{
            //useEffect훅
            memorizedState : {

            }
        }
    }
}

고차 컴포넌트

사용자 인증 정보에 따라서 인증된 사용자에게는 개인화된 컴포넌트를, 그렇지 않은 사 ㅇ자에게는 별도로 정의된 공통 컴포넌트를 보여주는 로직이 있다고 가정하자. 이런 경우 고차 컴포넌트가 매우 유용할 수 있다.

interface LoginProps {
    loginRequired?: boolean
}

function withLoginComponent<T>(Component: ComponentType<T>) {
    return function(props: T & LoginProps) {
        const {loginRequired,...rest} = props

        if(loginRequired) {
            return <>로그인이 필요해요!</>
        }
        return <Component {...rest} />
    }
}


//로그인 여부, 로그인이 안된 사용자는 다른 컴포넌트를 보는 것이 
//모두 고차 컴포넌트의 역할로 위임된다.

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

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

물론 이런 인증 단계는 서버와 같이 자바스크립트 이전 단계에서 처리하는 것이 좋다! (middleWare같은,,? 아니면 서버사이드에서.,.?) 고차 컴포넌트가 with으로 시작하는 것은 일종의 관습이다!