728x90

들어가며...

 

최근 WebRTC 에 대한 관심이 생겨서 클론 코딩을 해보았다. 이를 정리하고자 포스팅을 시작했습니다.

WebRTC  : 출처 (MDN)
(Web Real-Time Communication)은 웹 애플리케이션과 사이트가 중간자 없이 브라우저 간에 오디오나 영상 미디어를 포착하고 마음대로 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는 기술입니다

 

흔히 ZEP, gather 와 같은 메타버스 플랫폼 경우에 WebRTC를 활용하여 실시간 스트리밍을 가능하게 해줍니다.

 

 

1단계 장치 정보 받기

WebRTC로 P2P 통신을 하기 이전에 우선 장치 정보를 받아올 필요가 있습니다. 이때 사용하는 함수가
navigator.mediaDevices.getUserMedia 입니다. 이때 MediaDevices.enumerateDevices() 을 이용하면 그전 단계에서 

사용 가능한 디바이스 정보를 가져올 수 있습니다.

  const getMediaStream = useCallback(
    async (faceMode?: string) => {
      if (localStream) {
        return localStream;
      }

      try {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const videoDevices = devices.filter(
          (divice) => divice.kind === 'videoinput'
        );
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: {
            width: { min: 640, ideal: 1280, max: 1920 },
            height: { min: 360, ideal: 720, max: 1080 },
            frameRate: { min: 16, ideal: 30, max: 30 },
            facingMode: videoDevices.length > 0 ? faceMode : undefined,
          },
        });
        setLocalStream(stream);
        return stream;
      } catch (error) {
        console.log('failed to get stream', error);
        setLocalStream(null);
        return null;
      }
    },
    [localStream]
  );

 

 

2단계 PEER 연결하기

 

복잡한 P2P 연결에 대해서 알기 위해선 우선 네트워크에 관한 지식이 필요합니다. 비디오 스트리밍의 경우 데이터를 서버를 통해 직접적으로 전달하면 많은 부하가 생깁니다. 따라서 P2P 연결을 하는데, 이를 위해선 상대방의 IP 주소가 필요합니다. 이때 사용할 기술들은 보통 NAT, STUN 서버 ,ICE 입니다.

NAT (Network Address Translation)와 STUN

  • NAT는 사설 네트워크(private network) 내에서 사용하는 private IP 주소를 공인 IP(public IP 주소)로 변경하는 기술입니다. 이는 보안상 이유로 외부와의 직접적인 연결을 차단하고, 여러 장치가 하나의 공인 IP를 공유할 수 있게 해줍니다.
  • STUN (Session Traversal Utilities for NAT) 서버는 NAT 방화벽을 통과할 수 있도록 도와주는 역할을 합니다. 이 서버는 클라이언트에게 자신의 공인 IP 주소포트 번호를 알려주어, 외부에서 접근할 수 있도록 합니다. 하지만 STUN만으로는 NAT 뒤에 있는 장치 간의 연결을 완전히 해결할 수 없을 때도 있습니다.

ICE (Interactive Connectivity Establishment)

  • ICE는 WebRTC에서 두 피어 간의 연결을 설정할 때 최적의 경로를 찾는 프레임워크입니다. ICE는 STUN과 TURN 서버를 활용하여, NAT 뒤에 있는 두 장치가 서로 연결할 수 있도록 도와줍니다.
    • STUN은 NAT 방화벽을 통과할 수 있도록 공인 IP와 포트를 알려주는 데 사용됩니다.
    • TURN은 직접적인 연결이 불가능한 경우 중계 서버를 통해 연결을 지원합니다.

 

따라서 우리는 장치정보를 바탕으로 peer를 생성할 것 입니다. 이때 구글의 무료 스턴 서버를 사용하였고, 연결이 되면 peer의 stream을 통해서 p2p 연결 후 데이터를 주고 받습니다.

  const createPeer = useCallback(
    (stream: MediaStream, initiator: boolean) => {
      const iceServers: RTCIceServer[] = [
        {
          urls: [
            'stun:stun.1.google.com:19302',
            'stun:stun1.1.google.com:19302',
            'stun:stun2.1.google.com:19302',
            'stun:stun3.1.google.com:19302',
          ],
        },
      ];
      const peer = new Peer({
        stream,
        initiator,
        trickle: true,
        config: { iceServers },
      });

      peer.on('stream', (stream) => {
        setPeer((prevPeer) => {
          if (prevPeer) {
            return { ...prevPeer, stream };
          }
          return prevPeer;
        });
      });
      peer.on('error', console.error);
      peer.on('close', () => handleHangup({}));

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const rtcPeerConnection: RTCPeerConnection = (peer as any)._pc;

      rtcPeerConnection.onconnectionstatechange = async () => {
        if (
          rtcPeerConnection.iceConnectionState === 'disconnected' ||
          rtcPeerConnection.iceConnectionState === 'failed'
        ) {
          handleHangup({});
        }
      };

      return peer;
    },
    [handleHangup]
  );

 

 

때 보편적으로 RTCPeerConnection 를 통해서 연결을 합니다. 하지만 해당 프로젝트에서는  simple-peer 라이브러리를 활용하여 해당 단계를 쉽게 구현할 수 있습니다.

 

이를 통해서 스턴 서버를 통해서 ice 정보를 받고 스트리밍이 된다면 상대방의 stream 정보를 받아서 peer에 할당합니다. 

 

이후 해당 함수를 각각 전화거는 사람과 받는 사람이 사용하여 연결을 완료할 수 있습니다.

const completePeerConnection = useCallback(
    async (connectionData: {
      sdp: SignalData;
      ongoingCall: OngoingCall;
      isCaller: boolean;
    }) => {
      if (!localStream) {
        console.log('Missing the localStream');
        return;
      }

      if (peer) {
        peer.peerConnection?.signal(connectionData.sdp);
        return;
      }

      const newPeer = createPeer(localStream, true);

      setPeer({
        peerConnection: newPeer,
        partipantUser: connectionData.ongoingCall.participants.receiver,
        stream: undefined,
      });

      newPeer.on('signal', async (data: SignalData) => {
        if (socket) {
          // emit offer
          socket.emit('webrtcSignal', {
            sdp: data,
            ongoingCall,
            isCaller: true,
          });
        }
      });
    },
    [createPeer, localStream, ongoingCall, peer, socket]
  );

  const handleJoinCall = useCallback(
    async (ongoingCall: OngoingCall) => {
      setIsCallEnded(false);

      setOngoingCall((prev) => {
        if (prev) {
          return { ...prev, isRinging: false };
        }
        return prev;
      });
      const stream = await getMediaStream();
      if (!stream) {
        console.log('Could not get stream in handleJoinCall');
        return;
      }

      const newPeer = createPeer(stream, true);

      setPeer({
        peerConnection: newPeer,
        partipantUser: ongoingCall.participants.caller,
        stream: undefined,
      });

      newPeer.on('signal', async (data: SignalData) => {
        if (socket) {
          // emit offer
          socket.emit('webrtcSignal', {
            sdp: data,
            ongoingCall,
            isCaller: false,
          });
        }
      });
    },
    [createPeer, getMediaStream, socket]
  );

 

 

이때 ICE 정보의 경우 SDP(Session Description Protocol)에 담겨서 전송되고, 이는 Socket의 webritcSignal을 통해서 유저간 실시간으로 주고 받습니다. 그리고 해당 SDP 정보를 기반으로 p2p 연결을 하면 데이터가 stream 됩니다.

 

자세한 통화 프로젝트의 코드는 https://www.youtube.com/watch?v=dpWAqVSjgTM&list=PL63c_Ws9ecIS8ReV9MISpUGU71CK5cy0V 통해서 확인할 수 있습니다.

 

 

 

 

 

참고문헌 및 출처

https://webrtc.org/?hl=ko

 

WebRTC

An open framework for the web that enables Real-Time Communications (RTC) capabilities in the browser.

webrtc.org

 

https://developer.mozilla.org/ko/docs/Web/API/WebRTC_API

 

WebRTC API - Web API | MDN

WebRTC(Web Real-Time Communication)은 웹 애플리케이션과 사이트가 중간자 없이 브라우저 간에 오디오나 영상 미디어를 포착하고 마음대로 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는

developer.mozilla.org

https://www.npmjs.com/package/simple-peer

 

simple-peer

Simple one-to-one WebRTC video/voice and data channels. Latest version: 9.11.1, last published: 3 years ago. Start using simple-peer in your project by running `npm i simple-peer`. There are 296 other projects in the npm registry using simple-peer.

www.npmjs.com

 

 

https://www.youtube.com/watch?v=dpWAqVSjgTM&list=PL63c_Ws9ecIS8ReV9MISpUGU71CK5cy0V

https://gh402.tistory.com/38

 

[WebRTC] WebRTC란 무엇일까?

🎞 WebRTC란 무엇인가? Web Real-Time Communication의 약자로 웹/앱에서 별다른 소프트웨어 없이 카메라, 마이크 등을 사용하여 실시간 커뮤니케이션을 제공해주는 기술이다. 우리가 잘 알고있는 화상통

gh402.tistory.com

https://gh402.tistory.com/45

 

[WebRTC] NAT, ICE, STUN, TURN 이란? ( WebRTC를 이해하기 위해 필요한 지식들)

WebRTC를 사용하기 전, 기본적으로 익혀야 할 지식들!! 🌎 NAT(Network Address Translation) '나'는 누구인지 '이름'으로 구별할 수 있듯, 각 기기에도 자신만의 이름이 있다. 그것이 바로 IP이고 이 IP는 고

gh402.tistory.com

 

 

728x90

React 프로젝트에서 주로 React-router를 활용하여 SPA(Single Page Application)을 구현합니다. 하지만 SPA도 브라우저의 히스토리가 사용됩니다. 이를 간단하게 웹 API를 사용하여 약식으로 구현해보고자 합니다.

 

완성된 프로젝트의 소스코드 링크는 아래와 같습니다.

 

https://github.com/suhong99/spa_simple_router

 

GitHub - suhong99/spa_simple_router: React와 History API 사용하여 SPA Router 기능 구현

React와 History API 사용하여 SPA Router 기능 구현. Contribute to suhong99/spa_simple_router development by creating an account on GitHub.

github.com

 

구현할 내용은 about과 main 페이지를 직접 구현한 라우터를 위해여서 이동하는 것이고 이때 브라우저의 뒤로가기 버튼을 통해서 전 페이지로 이동할 수 있어야 합니다.

 

 

구현하기

우선 SPA에 사용할 브라우저 API를 먼저 언급하려고 합니다.

History API 를 통해서  실제 페이지를 이동했던 것 처럼 방문했던 페이지들을 기록해야 합니다.

History 객체란

  • 브라우저가 관리하는 객체로, 사용자가 방문한 페이지의 URL 정보를 담고 있습니다.
  • pushState 메서드를 호출하면 브라우저의 히스토리 스택에 새로운 기록이 추가됩니다.

이후 우리가 브라우저 상단의  뒤로가기 버튼 클릭시에 windown의 popState 이벤트가 발생하게 됩니다. 이 경우에 

히스토리 스택에서 이전 URL을 참고하게 됩니다.

 

이제 실제 구현해보려고 합니다.

Router

흔히 React-Router 라이브러리를 사용하게 되면 App에 browswer라우터를 등록하게 됩니다. 따라서 저희는 우선 Router를 먼저 구현해보려고 합니다.

import React, {
  Children,
  useEffect,
  useState,
  ReactNode,
  isValidElement,
} from 'react';

interface RouterProps {
  children: ReactNode;
}

const Router: React.FC<RouterProps> = ({ children }) => {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);

  useEffect(() => {
    const onPopState = () => setCurrentPath(window.location.pathname);
    window.addEventListener('popstate', onPopState);

    return () => window.removeEventListener('popstate', onPopState);
  }, []);

  return (
    <>
      {Children.map(children, (child) => {
        if (isValidElement(child) && child.props.path === currentPath) {
          return child;
        }
        return null;
      })}
    </>
  );
};

export default Router;

 

우리는 url의 경로가 바뀌게 되면 보여주는 컴퍼넌트가 달라집니다. 이때 달라지는 컴퍼넌트가 페이지 단위가 됨녀 SPA가 완성이 되는 것입니다.

.따라서 App을  Router로 감싼 이후에 내부에 있는 Route를 통해서 각각의 경로와 경로에 일치하는 컴퍼넌트를 감지할 것 입니다.

 

이때 뒤로가기가 작동하게 하기 위해서 useEffect를 통해서 사이드 이펙트를 감지할 것이고,  발생할 때마다 현재 경로를useState로 선택한 지역상태를 통해 관리할 것 입니다.

그리고 이를 통해서 컴퍼넌트가 렌더링 가능한 컴퍼넌트이고, 경로가 일치하면 반환하는 식으로 구현하였습니다.

  {Children.map(children, (child) => {
        if (isValidElement(child) && child.props.path === currentPath) {
          return child;
        }
        return null;

유의사항 : 이때 map은 js의 map 이 아닌 리엑트 Children의 map입니다.

Route

이제 하위에서 경로와 렌더링할 컴퍼넌틀틀 받는 Route 컴퍼넌트를 작성할 것 입니다.

import React, { ReactElement } from 'react';

interface RouteProps {
  path: string;
  component: ReactElement;
}

const Route: React.FC<RouteProps> = ({ component }) => component;

export default Route;

 

컴퍼넌트와 props를 등록하여서 Router에서 사용할 수 있도록 만들어줍니다.

 

useRouter 훅

import { useCallback } from 'react';

const useRouter = () => {
  const push = useCallback((path: string) => {
    window.history.pushState({}, '', path);
    const popStateEvent = new PopStateEvent('popstate');
    window.dispatchEvent(popStateEvent);
  }, []);

  return { push };
};

export default useRouter;

이후 페이지 이동시에는 pushState함수를 이용해서 History API에 방문하는 페이지를 기록해줍니다.

 

 

해당 훅의 사용방법은 아래와 같습니다.

 

import React from 'react';
import useRouter from '../router/hook/useRouter';

interface NaviButtonProps {
  text: string;
  url: string;
}

const NaviButton: React.FC<NaviButtonProps> = ({ text, url }) => {
  const { push } = useRouter();

  const handleClick = () => {
    push(url);
  };

  return (
    <button
      onClick={handleClick}
      style={{
        backgroundColor: 'rgba(0, 0, 0, 0.7)',
        color: 'white',
        border: 'none',
        borderRadius: '8px',
        padding: '10px 20px',
        cursor: 'pointer',
      }}
    >
      {text}
    </button>
  );
};

export default NaviButton;

 

 

 

 

참고문헌

 

https://developer.mozilla.org/ko/docs/Web/API/Window/popstate_event

 

popstate - Web API | MDN

Window 인터페이스의 popstate 이벤트는 사용자의 세션 기록 탐색으로 인해 현재 활성화된 기록 항목이 바뀔 때 발생합니다. 만약 활성화된 엔트리가 history.pushState() 메서드나 history.replaceState() 메서

developer.mozilla.org

 

https://ko.react.dev/reference/react/isValidElement

 

isValidElement – React

The library for web and native user interfaces

ko.react.dev

https://developer.mozilla.org/ko/docs/Web/API/History_API

 

History API - Web API | MDN

History API는 history 전역 객체를 통해 브라우저 세션 히스토리(웹 익스텐션 히스토리와 혼동해서는 안 됩니다.)에 대한 접근을 제공합니다. 사용자의 방문 기록을 앞뒤로 탐색하고, 방문 기록 스택

developer.mozilla.org

 

728x90

들어가며

redux는 어떻게 상태 관리를 하는가에 대해서 알아보기 위해서 createStore에 대해서 알아보려고 합니다.

createStore는 deprecated인데 왜 해당 함수에 대해서 알아보냐에 대한 답변은 아래와 같습니다.

 

The standard method for creating a Redux store. It uses the low-level Redux core 
createStore
 method internally, but wraps that to provide good defaults to the store setup for a better development experience.

 

공식 문서에 의하면 configureStore 는 middleware등 추가적인 기능을 createStore 함수를 기반으로 같이 쓸 수 있게 만든 함수이기 때문입니다.

따라서 핵심 상태 관리 원리는 여전히 createStore에 남아있고 다만 유저는 configureStore를 사용하는 것이 권장되기에 derpecated 된 것 입니다.

 

createStore 살펴보기

 

https://github.com/reduxjs/redux/blob/master/src/createStore.ts

 

코드 전문은 아래와 같습니다. 

export function createStore<
  S,
  A extends Action,
  Ext extends {} = {},
  StateExt extends {} = {}
>(
  reducer: Reducer<S, A>,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, UnknownIfNonSpecific<StateExt>> & NoInfer<Ext>

export function createStore<
  S,
  A extends Action,
  Ext extends {} = {},
  StateExt extends {} = {},
  PreloadedState = S
>(
  reducer: Reducer<S, A, PreloadedState>,
  preloadedState?: PreloadedState | undefined,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, UnknownIfNonSpecific<StateExt>> & NoInfer<Ext>
export function createStore<
  S,
  A extends Action,
  Ext extends {} = {},
  StateExt extends {} = {},
  PreloadedState = S
>(
  reducer: Reducer<S, A, PreloadedState>,
  preloadedState?: PreloadedState | StoreEnhancer<Ext, StateExt> | undefined,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, UnknownIfNonSpecific<StateExt>> & NoInfer<Ext> {
  if (typeof reducer !== 'function') {
    throw new Error(
      `Expected the root reducer to be a function. Instead, received: '${kindOf(
        reducer
      )}'`
    )
  }

  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
    )
  }

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error(
        `Expected the enhancer to be a function. Instead, received: '${kindOf(
          enhancer
        )}'`
      )
    }

    return enhancer(createStore)(
      reducer,
      preloadedState as PreloadedState | undefined
    )
  }

  let currentReducer = reducer
  let currentState: S | PreloadedState | undefined = preloadedState as
    | PreloadedState
    | undefined
  let currentListeners: Map<number, ListenerCallback> | null = new Map()
  let nextListeners = currentListeners
  let listenerIdCounter = 0
  let isDispatching = false


  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = new Map()
      currentListeners.forEach((listener, key) => {
        nextListeners.set(key, listener)
      })
    }
  }

 
  function getState(): S {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState as S
  }

  
  function subscribe(listener: () => void) {
    if (typeof listener !== 'function') {
      throw new Error(
        `Expected the listener to be a function. Instead, received: '${kindOf(
          listener
        )}'`
      )
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api/store#subscribelistener for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    const listenerId = listenerIdCounter++
    nextListeners.set(listenerId, listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api/store#subscribelistener for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      nextListeners.delete(listenerId)
      currentListeners = null
    }
  }

 
  function dispatch(action: A) {
    if (!isPlainObject(action)) {
      throw new Error(
        `Actions must be plain objects. Instead, the actual type was: '${kindOf(
          action
        )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
      )
    }

    if (typeof action.type !== 'string') {
      throw new Error(
        `Action "type" property must be a string. Instead, the actual type was: '${kindOf(
          action.type
        )}'. Value was: '${action.type}' (stringified)`
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    listeners.forEach(listener => {
      listener()
    })
    return action
  }

  function replaceReducer(nextReducer: Reducer<S, A>): void {
    if (typeof nextReducer !== 'function') {
      throw new Error(
        `Expected the nextReducer to be a function. Instead, received: '${kindOf(
          nextReducer
        )}`
      )
    }

    currentReducer = nextReducer as unknown as Reducer<S, A, PreloadedState>

   
    dispatch({ type: ActionTypes.REPLACE } as A)
  }

 
  function observable() {
    const outerSubscribe = subscribe
    return {
     
      subscribe(observer: unknown) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError(
            `Expected the observer to be an object. Instead, received: '${kindOf(
              observer
            )}'`
          )
        }

        function observeState() {
          const observerAsObserver = observer as Observer<S>
          if (observerAsObserver.next) {
            observerAsObserver.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }


  dispatch({ type: ActionTypes.INIT } as A)

  const store = {
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  } as unknown as Store<S, A, StateExt> & Ext
  return store
}

 

결국 요약만 하면  currentState를 통해 상태를 기억하고  외부에서 받아온 reducer를 통해서 상태를 업데이트 합니다. 
Map으로 선언한 listener를 통해서 구독자를 관리합니다. 이후 반환값으로 상태를 얻는 getState, 구독하는 subscribe, 상태를 업데이트하기 위한 dispatch함수들을 반환합니다.

(상태 관리에 좀 더 집중하기 위해 여타 에러처리와 replaceReducer , observable에 대해서는 넘어가겠습니다.)


이제 너무 방대한 양이기에 이를 간단하게 요약해서 코어만 추출해보려고 합니다.

 

createStore 코어만 작성하기

const createStoreCore = (reducer) => {
  let state;
  let listeners = new Map();
  let listenerIdCounter = 0;

  const getState = () => state;

  const subscribe = (listener) => {
    const id = listenerIdCounter++;
    listeners.set(id, listener);
    
    return () => {
      listeners.delete(id);
    };
  };

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  };

  dispatch({});

  return { getState, subscribe, dispatch };
};

 

상태를 수정하는 것은 reducer를 통해서 받아서 상태가 순수할 수 있도록 하고,
listners를 통해 구독시킵니다. 이후 dispatch를 할 때 (상태가 수정될 때) 는  구독자에게 알려야 하고, 구독이 끝났을 때는 listeners에서 제거해야 합니다.

 

이전에 '리엑트 훅을 활용한 마이크로 상태 관리 책'을 통해서  확인한  zustand, jotai, valtio 등과 원리는 비슷하네요.
다만 상태는 createStore 내부에서 클로저를 통해 관리하고 있습니다.


react에서는 어떻게 리렌더링이 되는가?

redux에서는 상태 관리와 리렌더링의 주체가 관심사가 나뉘어져 있습니다.

리렌더링은 react-redux 라이브러리의 훅들을 기반으로 진행됩니다.

대부분의 상태관리 라이브러리는 외부 상태를 useSyncExternalStore 훅을 통해서 합니다.

이때 redux에서는 구 버전(useSyncExternalStore 훅이 나온 18버전 이전)의 리엑트에 대응하기 위해서 

use-sync-external-store  을 활용합니다.

 

해당 코드는 useSelector 내부에서 확인할 수 있습니다.

export const initializeUseSelector = (fn: uSESWS) => {
  useSyncExternalStoreWithSelector = fn
}

 

dispatch 시 내부에서 listener에게 상태 변화가 감지가 되고, 콜백함수로 작동하여 리렌더링을 일으킵니다.

 

 

참고문헌

 

https://redux-toolkit.js.org/api/configureStore

 

configureStore | Redux Toolkit

 

redux-toolkit.js.org

https://ko.react.dev/reference/react/useSyncExternalStore

 

useSyncExternalStore – React

The library for web and native user interfaces

ko.react.dev

https://github.com/reduxjs/react-redux

 

GitHub - reduxjs/react-redux: Official React bindings for Redux

Official React bindings for Redux. Contribute to reduxjs/react-redux development by creating an account on GitHub.

github.com

 

+ Recent posts