🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] 상태 관리 (+라이브러리)
    Front-End/React 2023. 12. 1. 18:54
    🍀 목차
    전역 상태
    상태 관리의 필요성
    대표적 라이브러리와 콘셉트
    계속되는 연구들

     

    전역 상태

     

     상태는 변화하는 동적인 데이터이다. 웹 페이지에서 데이터는 사용자와 상호작용을 통해 변경되기도 하고, 그렇게 변한 데이터가 UI에 나타나기도 한다. React에서는 Component(컴포넌트)들이 State(상태)라는 객체를 가지며, 변경되면 컴포넌트는 리렌더링되어 반영하게 된다. 이러한 상태들은 보통 컴포넌트 안에서 관리되나, 단일 컴포넌트를 넘어 전역적으로 프로젝트에서 사용되는 상태가 될 수도 있다. 

     보통 전역이란 단어가 붙으면 프로그램 어디서나 접근할 수 있다를 의미하는데, 전역 상태도 같다. 또한, 상태기에 컴포넌트가 변경을 감지해 리렌더링 해야 한다.

     

     전역 상태의 예시에는 무엇이 있을까?

    다크모드나 로그인 사용자 정보, 국제화 설정 같은 프로젝트나 페이지에 존재하는 독립적인 컴포넌트들이 동일한 상태를 가져야 할 때를 예로 들 수 있을 것이다.

     

     

    상태 관리의 필요성

     단순하게는 무수히 많은 데이터를 관리하기 어렵기 때문에 필요할 것이다. 전역 상태처럼 한 컴포넌트만 사용되는 것이 아닐 경우, 복잡하게 연결되어 있는 데이터를 보다 효율적으로 관리할 필요가 있다.

     React에서 컴포넌트 간 데이터를 전달해야 하는 경우를 예로 들어보자. 상태가 변경된 후, 해당 데이터를 하위 컴포넌트로 보내야 한다. 변경 감지부터 알아보자.

     

    역 상태의 변경 감지

    해당 컴포넌트 안에서만 사용하는 지역 상태는 어떻게 변경을 감지할까?

     상태 변경 전과 후의 Virtual DOM을 비교하여 변경 부분만 실제 DOM에 반영시키게 된다(리렌더링이 일어남). 

    이 과정에서 React의 상태들은 불변성을 유지해야 하는데, 이는 새로운 상태를 새로운 참조로 하여 변경을 감지하기 때문이다(값을 직접 변경하면 비교 대상도 사라지기에 변경 여부를 알기도 어렵다). 

     

     React 프로젝트를 수행하며 useState로 상태를 변경시켰음에도 리렌더링 되지 않았던 경우가 있었다. 이는 원시 타입(Primitive Type) 값과 참조 타입(Reference Type) 값의 차이를 몰랐기 때문이었다. 원시 타입(Primitive Type)의 경우는 값이 변경될 때, 새로운 메모리 영역에 할당하게 된다. 불변성을 갖고 있는 것. 

    // Primitive Type
    let temp = 'temp data...1';
    let temp2 = temp;
    temp2 = temp2.slice(0,-1) + '2';
    
    console.log(temp);
    //temp data...1
    console.log(temp2);
    //temp data...2

     

     

    배열이나 객체 같은 참조 타입(Reference Type)은 다르다.

    // Reference Type
    
    const person = {
    	name: 'Young'
    };
    
    const student = person;
    person.gender = 'F';
    
    console.log(student);
    // {name: 'Young', gender: 'F'}

     

     원본에 대한 참조 값이 복사되어 전달되기에 둘 중 어느 객체라도 변경이 일어나면 영향을 주고받는다. 이렇게 될 경우 변경여부를 파악하기 어려워지는 것이다. 

    특정 컴포넌트 안에서만 관리되는 지역상태의 경우에는 이러한 차이를 숙지하여 불변성을 지키며 관리해야 한다. 

     

     

    데이터의 전달

     이제 지역 상태가 변경되었으니 해당 변경값을 사용하는 컴포넌트에 동기화를 위해 전달해야한다.

     컴포넌트들이 부모(상위) - 자식(하위) 컴포넌트 관계라면 필요로 하는 컴포넌트까지 props를 전달하게 하면 될 것이다. 그리고 이 방식은 구조가 복잡해질수록 관리하기 어려워진다.

     

    상위 컴포넌트에서 하위 컴포넌트로 props를 내려보낸다.
    Prop drilling

     

     Foo, Bar 컴포넌트는 해당 데이터를 사용하지 않음에도 NeedState컴포넌트를 위해 props를 전달해야 한다. 이런 과정을  Prop drilling이라고 한다. 단순 전달만을 위한 행위가 몇십, 몇백 개로 존재한다면 어디에서 데이터가 사용되는지 추적하기도 힘들어진다.

     

     Prop drilling을 피하기 위한 방법 중 하나는 (멀리 돌아왔지만) 보다 효율적인 상태 관리이다.

    Prop drilling 뿐 아니라 전역 상태를 관리하는 데에도 상태 관리가 필요하다. 상태를 컴포넌트 트리 어디에서든지 읽어올 수 있다면 데이터 관리도 편해지고 최적화도 제공한다면 성능까지 높일 수 있을 것이다.

     

     React는 이러한 상태 관리에 대해 확실한 가이드라인을 제공하고 있지 않고, 다양한 접근 방식으로 상태 관리를 하는 라이브러리가 생겨나게 되었다.

    대표적인 상태 관리 라이브러리를 알아보며 어떤 설계로 상태 관리를 하고 있는지 알아보자.


    대표적 라이브러리와 콘셉트

    Redux

    • Redux는 일관적으로 동작하고, 서로 다른 환경(클라이언트, 서버, 네이티브)에서도 실행되며, 테스트하기 쉬운 앱을 작성하도록 도와준다.
    • 앱의 상태와 로직을 중앙화하여 실행 취소/다시 실행, 영속적인 상태 등의 강력한 기능을 사용할 수 있게 한다.
    • 어떤 UI 레이어에서도 동작하며, 거대한 애드온 생태계를 갖고 있다.

     

     Redux는 React 같은 뷰 라이브러리와 함께 사용할 수 있는 JS 앱을 위한 예측 가능한 상태 컨테이너이다. 애드온 생태계가 굉장히 넓기 때문에 필요한 기능이 있다면 추가 플러그인을 설치하여 사용할 수 있다.

     Redux는 특정 데이터가 언제 변경되었는지, 어디에서 왔는지에 대한 질문에 도움을 주기 위해 만들어졌다. 데이터를 관리하는 데 있어서 가장 짧거나 빠른 방법으로 설계되지 않았다. 3가지 원칙도 존재한다. 그렇기에 반복적으로 사용되는 코드(Boiler Plate : 보일러 플레이트)가 있다. 섣부르게 사용하기보다는 아래와 같은 상황일 때 도입하는 것이 좋다.

     

    1. 여러 위치에서 필요한 많은 양의 애플리케이션 상태가 존재한다. 👉 Redux는 Store을 활용하여 상태를 한 곳에서 관리한다. 이를 활용한다면 관리가 용이해질 것이다.

    2. 해당 앱의 상태는 자주 변경되고, 업데이트 로직이 복잡하며, 어떻게 업데이트되는지 확인해야 한다. 👉 Redux는 Redux DevTools를 사용하여 디버깅을 쉽게 할 수 있다. 변경은 단방향으로 이루어지기에 state 변경이 언제, 어디서, 왜, 어떻게 바뀌었는지 추적이 가능하다.

    3. 많은 사람들이 작업하는 앱이다.

     

     

     Redux는 Flux 패턴 + Reducer를 사용한다. Flux 디자인 패턴은 MVC 아키텍처 디자인 패턴의 단점을 보완하기 위해 발표되었다. MVC 패턴은 데이터(Model이 관리), 출력(View가 관리), 컨트롤러(Model과 View를 이음)를 분리하여 유지보수성을 높였으나 규모가 커질수록 복잡한 데이터 흐름을 가지게 되었다. 

     

    점점 복잡해지는 시스템

     

     View와 Model은 규모가 커질수록 서로 간의 의존성이 높아진다. 또한 컨트롤러도 여러 개 생긴다면 더더욱 깔끔해지기 어려워진다. 웹 페이지에서 사용자와의 상호작용은 View에서 일어나기에 View에서 Model을 변경시킬 수도 있을 것이다.  Flux 패턴은 이러한 양방향 데이터 흐름을 개선시키기 위해 단방향 데이터 흐름을 가지도록 설계되었다. Redux 코드들과 함께 생각해 보았다.

     

    Flux 패턴

    • Action : 상태를 업데이트하기 위해 사용되는 객체. typepayload 프로퍼티를 가진다. type은 해당 작업에 대한 설명이 담긴 문자열, payload는 무엇이 일어났는지에 대한 추가 정보를 담는다.
    const addTodoAction = {
      type: 'todos/todoAdded',
      payload: 'Buy milk'
    }
    
    // type은 "domain/eventName" 형식으로 domain에는 작업이 속한 기능, eventName은 특정 행위를 서술한다.
    • Dispatcher :  Action이 Dispatcher에게 전달되면, Dispatcher는 Action 객체를 받아 어떤 행동을 할지 결정한다. 중앙 허브 역할이다.
       Redux에서는 Dispatch 함수를 통해 Action 객체를 Reducer로 전달, Reducer는 전달받은 Action 객체의 type값에 따라 상태를 변경시킨다. Redux는 dispatch 함수로 Action 객체를 전달하며 무슨 일이 벌어지는지 기록을 남긴다. 이러한 상태 트리 변화를 알기 위해 개발자는 Reducer를 작성해야 한다. 
    // Action Creators. Action 객체를 반환한다. 
    const increment = () => {
      return {
        type: 'counter/increment'
      }
    }
    
    // dispatch 메서드로 Action 객체를 전달한다.
    store.dispatch(increment())
    
    console.log(store.getState())
    // {value: 2}

     

    const initialState = { value: 0 }
    
    // 받은 Action 객체의 type에 따라 로직 실행, 변경된 상태를 리턴한다. 
    function counterReducer(state = initialState, action) {
      // Check to see if the reducer cares about this action
      if (action.type === 'counter/increment') {
        // If so, make a copy of `state`
        return {
          ...state,
          // and update the copy with the new value
          value: state.value + 1
        }
      }
      // otherwise return the existing state unchanged
      return state
    }

     

    • Store : 데이터(상태)를 담고 있다. Redux의 3가지 원칙 중 하나는 모든 상태가 하나의 저장소 안에 하나의 객체 트리 구조로 저장된다는 것이다. Store 객체는 Reducer를 전달하여 생성, 현재의 상태 값을 반환하는 getState 메서드가 존재한다. 매번 동일한 데이터를 읽는 상황이 많아지면 Selectors 함수를 활용하여 로직을 줄일 수 있다. 
    import { configureStore } from '@reduxjs/toolkit'
    
    const store = configureStore({ reducer: counterReducer })
    console.log(store.getState())
    // {value: 0}
    
    //Selectors 사용
    const selectCounterValue = state => state.value
    
    const currentValue = selectCounterValue(store.getState())
    console.log(currentValue)
    // 2
    • View : 출력을 담당한다. Store에서 변경이 발생하면, View는 변경된 점을 가져와 렌더링 한다. 

     

     

     이렇게 Redux가 기존의 문제를 어떻게 해결하고자 했는지, 어떻게 변경을 추적하는지를 알아보았다. 

    다른 라이브러리는 어떨까?

     

     

    MobX

    • 간단한 코드로 데이터를 변경한다. 비동기 과정에도 변경사항을 찾아내고 사용 중인 곳으로 전달한다.
    • 데이터의 모든 관계를 의존 트리로 만들어 필요한 경우에만 연산을 실행한다. 컴포넌트 최적화 작업을 할 필요가 없다.
    • 코드 분리, 테스트가 쉽게 가능하다.

     

    출처 : https://ko.mobx.js.org/README.html

     

     MobX는 Reactive Programming으로부터 영감을 받은 라이브러리이다. 어떤 값이 변경되면 그를 Subscribing 하는 Observer에게 리렌더링 하도록 알림을 준다. 

     

    import { makeObservable, observable, action } from "mobx"
    
    class Todo {
        id = Math.random()
        title = ""
        finished = false
    
        constructor(title) {
            makeObservable(this, {
                title: observable,
                finished: observable,
                toggle: action
            })
            this.title = title
        }
    
        toggle() {
            this.finished = !this.finished
        }
    }

     

     상태를 정의하고 이를 MobX가 추적할 수 있도록 obsevable로 표시한다. toggle의 경우에는 action으로 선언되었는데, 기본적으로 state를 변경하는 코드 조각임은 Redux와 비슷해 보인다. 다른 점이 있다면 MobX의 action은 직접 state를 변경하는 메서드이다.

     

    import { makeObservable, observable, computed } from "mobx"
    
    class TodoList {
        todos = []
        get unfinishedTodoCount() {
            return this.todos.filter(todo => !todo.finished).length
        }
        constructor(todos) {
            makeObservable(this, {
                todos: observable,
                unfinishedTodoCount: computed
            })
            this.todos = todos
        }
    }

     

     변경 후에는 파생(derivation)을 발생시킨다. MobX는 두 종류로 derivation을 구분한다.

    • computed 값 : 위에서 toggle은 observable state인 finished를 토글 하는 action이다. computed로 선언된 unfinishedTodoCount는 finished가 수정되면 자동으로 업데이트한다. 
    • reaction : 그리고 이런 state, computed 값의 변화에 따른 행동을 정하기 위해 reaction을 사용한다. computed 값과 유사하지만, 값 생성 대신 콘솔 출력이나 네트워크 요청 같은 React 컴포넌트 트리를 점진적으로 업데이트하는 등의 부수 효과를 생성한다. React의 컴포넌트를 observer 함수를 이용하여 감싸면 반응형으로도 만들 수 있다. 

     

     MobX는 store의 개수에 제한도 없기에 분리에 용이하다. 다만 잘못된 설계를 할 경우 상태 변경 시 다수의 store에 영향을 줄 수도 있다. 테스트나 유지보수 측면에서 대규모 프로젝트가 될 경우 관리가 어려워질 수 있는 것.

     

    Context API

    우선, Context API는 별도의 설치가 필요하지 않은 React 내장 기능이다. 후에 설명할 라이브러리가 Context API와 관련이 있어 추가했다.

     Context API는 props를 넘겨주지 않더라도 컴포넌트 트리 전체에 데이터를 제공시킬 수 있다. 데이터 값이 변경되면 모든 하위 컴포넌트에게 널리 방송한다.

    const MyContext = React.createContext(defaultValue);
    
    <MyContext.Provider value={/* 어떤 값 */}>

     

    Context 객체를 만들게 되면 React 컴포넌트인 Provider가 들어있다. Provider는 Context를 구독하는 컴포넌트들에게 Context 변화를 알리는 역할을 한다. Provider로 공유하고자 하는 값을 value라는 prop로 설정하면, 자식 컴포넌트에서는 해당 값에 바로 접근할 수 있다. Provider 하위에 또 다른 Provider를 배치하는 것도 가능하다(하위 Provider 값이 우선시 됨). Provider 하위에서 Context를 구독하는 모든 컴포넌트는 value prop가 바뀐다면 리렌더링 된다. 이 리렌더링이라는 행위 때문에 Context API로 전역 상태를 관리할 때는 정말 해당 상태가 모든 하위 컴포넌트에 리렌더링을 발생시켜야 하는 상태인지 다시 고민해 볼 필요가 있다.

     

    중첩된 Provider.

     

     

    Recoil

    • 작고(Atom) React스럽다. - Atoms
    • 순수 함수와 효율적인 구독으로 데이터를 관리한다. - Selectors

     

     Context API도 명확한 한계(Provider의 관리하기 어려울 정도의 래핑, 잘못 다룰 경우의 리렌더링)가 있어 React 팀은 Atomic 한 Recoil을 만들었다. Redux, MobX가 다른 뷰 라이브러리에서도 사용되는 것과 다르게 Recoil은 React만을 위해 생겨났다.

     Atom은 상태의 일부를 나타낸다. 어떤 컴포넌트에서나 읽고 쓸 수 있으며, 이 값을 읽는 컴포넌트는 암묵적으로 atom을 구독한다. 변화가 생기면 구독하는 모든 컴포넌트가 재 렌더링되는 결과가 나온다. Context API가 하위 컴포넌트에게 모두 영향을 준다면 Recoil은 구독 컴포넌트만 신경 쓰면 된다. 

     

     Atom의 변경은 순수 함수인 Selector로 이루어진다. 

    순수 함수
    동일한 인자에 대해 항상 똑같은 값을 리턴하는 함수이다. 값을 변형하거나 외부의 상태도 변화시키지 않기에 부작용, 부수 효과(Side-Effect : 의도하지 않는 결과가 발생하는 경우)가 없다. 

     

     컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors 변경 시 렌더링 된다. selector 메서드를 사용해 정의한다.

     

    function selector<T>({
      key: string,
    
      get: ({
        get: GetRecoilValue
      }) => T | Promise<T> | RecoilValue<T>,
    
      set?: (
        {
          get: GetRecoilValue,
          set: SetRecoilState,
          reset: ResetRecoilState,
        },
        newValue: T | DefaultValue,
      ) => void,
    
      dangerouslyAllowMutability?: boolean,
    })

     

    • key는 내부적으로 atom을 식별하는 데 사용되는 고유 문자열이다. 이 문자열은 다른 atom와 selector에 대해 고유해야 한다. 
    • get은 계산될 함수로 전달되는 get 인자를 통해 atoms와 다른 selectors에 접근할 수 있다. 이 함수에 전달된 모든 atom과 selector는 암시적으로 selector에 대한 의존성 목록에 추가된다. 자동으로 종속 관계가 생겨, 참조하는 atoms나 selectors가 업데이트되면 해당 함수도 다시 실행된다. 
    • set함수는 선택적으로 제공하며 atom 값 변경을 도와준다. 원하는 state로 수정할 수 있다.
    • dangerouslyAllowMutability : Recoil은 불변성을 유지하기 위해 Object.freeze 같은 deep freeze로 객체에 프로퍼티를 추가할 수 없게 만들었다. 하지만 이로 인해 오류가 발생하는 경우가 있다면 선택적으로 deep freeze기술을 못 쓰도록 하는 옵션이다. 

     

     

     

     Recoil까지 React의 대표적인 상태 관리 라이브러리를 알아보았다.

    공통적으로는 리렌더링에 대한 최적화, 데이터 흐름을 잘 따라갈 수 있도록 설계를 한 게 돋보인다. 조사하면서 느낀 건 전역 상태 관리는 항상 조심해야 한다는 것이다. 정말 해당 상태를 전역으로 관리해야 하는지 고민해야겠다.

    아래부터는 React를 떠나 애플리케이션 상태 관리를 어떻게 할 것인지 여러 진영에서 채택하고 있는 Signal에 대한 소개이다.

     

    계속되는 연구들

    Preact의 Signals

    우선, Signals은 Vue, Solid, Preact 및 Angular등에서 채택하고 있는 오래전부터 JavaScript에서 사용되고 있던 상태 동기화에 대한 방법이다. 특정된 라이브러리라기보다는, 문제를 해결하는 방법이라고 이해했다. 특정 DOM 업데이트에 반응성을 사용한다.

     

    아래부터는 공식문서(번역) Preact 공식 문서에 작성된 Signals에 대한 소개를 읽으며 간단하게 정리해 봤다. 

    최적화를 위해 개발자는 보통 메모이제이션을 사용한다.

    Memoization(메모이제이션)
    이전 계산값을 저장함으로서 동일한 계산이 들어왔을 때 해당 계산값을 사용하여 실행 속도를 빠르게 하는 기술

     

     React에서는 useMemo, memo를 사용하여 불필요한 리렌더링을 막기 위해 값을 저장하는 메모이제이션을 활용한다. 다만 현실의 웹 페이지는 혼란스럽기 때문에, 이러한 최적화를 잘못된 곳에 한다면 비효율적인 경우도 있다. 개발자는 점점 혼란에 빠진다...

     

     Signal은 기본적으로 빠른 시스템을 제공한다. 주요 아이디어는 값이 포함된 시그널 객체를 전달하는 것이다. 컴포넌트는 시그널의 값이 아닌 시그널 객체를 보고 있기에 다시 렌더링 하지 않고도 시그널을 업데이트할 수 있다는 것.

     

    출처 : https://preactjs.com/blog/introducing-signals/

     

    // 이것이 전역 상태이고 전체 앱에서 이에 접근해야 하는 상황을 상상해보세요.
    
    let count = 0;
    
    function Counter() {
     return (
       <button onClick={() => count++}>
         value: {count}
       </button>
     );
    }

     

    위 코드는 명확하나 count가 변경되었는지 알 방법이 없기 때문에 작동하지는 않는다.

    시그널은 위처럼 명확한 모델을 사용하고자 했고, 이를 구현했다. Signal은 .value 속성을 가진 객체이며 value가 변경되면 자동으로 업데이트한다. 그래서 아래와 같은 코드 작성이 가능해졌다.

     

    // 이것이 전역 상태이고 전체 앱에서 이에 접근해야 하는 상황을 상상해보세요.
    
    const count = signal(0);
    
    function Counter() {
     return (
       <button onClick={() => count.value++}>
         Value: {count.value}
       </button>
     );
    }

     

    REPL에서 실행

     

     그리고 더 빠르게 하기 위해...

    const count = signal(0);
    
    // 아래처럼 하지 않고
    <p>Value: {count.value}</p>
    
    
    // … 우리는 시그널을 직접 JSX에 전달할 수 있습니다.
    <p>Value: {count}</p>
    
    // … 또는 돔 속성으로도 전달할 수 있습니다.
    //[번역 출처 : https://soobing.github.io/react/introducing-signals/]

     

    const count = signal(0);
    
    function Unoptimized() {
      // Re-renders the component when `count` changes:
      return <p>{count.value}</p>;
    }
    
    function Optimized() {
      // Text automatically updates without re-rendering the component:
      return <p>{count}</p>;
    }

     

     시그널의 값이 변경될 때 자동으로 컴포넌트를 리렌더링 하는 대신, 변경 사항을 직접 DOM에 바인딩시킨다. count는 텍스트를 표시하는 데만 사용되기에 텍스트로 렌더링 시키고, 변경 시 돔에서 직접 업데이트시킨다. 

     

     시그널은 값이 변경되면 신호를 보내고, 그에 대한 반응을 일으킨다. 해당 시그널을 사용하고 있는 모든 요소를 업데이트하기 위해 반응을 일으키게 되는 건데, 디자인 패턴 중 관찰자 패턴(MobX처럼) 기반이기에 신호를 구독한 요소들만 업데이트된다. 

     

     돔에 직접 업데이트하는 것은 위험하지 않나...? 정말 빠른가...?라는 생각이 들었는데 virtual DOM을 유지 관리하는 데 드는 오버헤드도 있기 때문에 Solid에서는 virtual DOM을 사용하지 않고 템플릿 리터럴 시스템을 사용하여 모든 변경 사항을 식별하여 격리되고 세분화된 업데이트로 돔의 특정 영역을 접근한다고 한다. 렌더링 속도에서도 인상적인 통계를 내고 있다는데 지표 확인은 링크에서 확인할 수 있다(preact signals도 보인다). + preact는 시그널을 virtual DOM에 최적화된 고유 구현으로 만들었다고 소개했다. 

     

     

    많고 다양한 라이브러리들 ...

     매 해마다 N Top React State Menagement Libraries 같은 Award가 존재하는 것 같다. 다양한 라이브러리들은 기존 흐름에 대한 단점을 해결한 것이기도 하고, 새로운 방법론을 제시하기도 한다. 예를 들어, Zuztand는 Redux등과 같은 Flux 패턴을 사용하지만 보일러플레이트를 제거함으로써 핵심 로직의 수를 줄였다. Redux가 안정적인 상태 유지를 위해 요구했던 강한 제약들을 해결하려고 한 것이다. 

     

    Zustand가 redux&#44; context의 어떤 점을 해결했는 지 보여주는 그림.
    Zustand의 소개에 존재하는 Redux, Context의 어떤 점을 해결했는 지 보여주는 글

     

     

    이 외에 Flux 패턴을 선호하지 않는 개발자는 Atomic 패턴을 사용하는 Recoil, Jotai를 도입할 수도 있을 것이다. 서버 상태를 전역 상태로 관리하는 경우는 서버 캐싱 전용 라이브러리인 React-query를 사용할 수도 있다.

     상태 관리에 답은 없다고 생각한다. React는 자유로운 생태계를 갖고 있기에 상태 관리에 대한 개발자의 선택지도 많다. 이 속에서 특정 요구 사항, 규모, 팀의 상황에 따라 알맞게 적용하는 것이 중요해 보인다.

     

     

     

     

    참고자료

    https://ko.legacy.reactjs.org/docs/faq-state.html

    https://jbee.io/react/thinking-about-global-state/

    https://medium.com/@yujso66/%EB%B2%88%EC%97%AD-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%9D%90%EB%A6%84-6e5ed0022e39

    https://www.stevy.dev/react-state-management-guide/

    https://ko.redux.js.org/

    https://junghan92.medium.com/%EB%B2%88%EC%97%AD-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-signal%EC%9D%98-%EC%A7%84%ED%99%94-4bd6a991d2f

    https://oliviakim.tistory.com/93

    https://ko.legacy.reactjs.org/docs/context.html

    https://ko.mobx.js.org/the-gist-of-mobx.html

    https://recoiljs.org/ko/docs/api-reference/core/selector/

    https://www.sitepoint.com/signals-fine-grained-javascript-framework-reactivity/

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼