ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11/16 TIL | React 상태 관리 라이브러리 useStore-ts! 마이크로스토어로 가는 길🚶🏻‍♀️
    📝 기록/매일의 기록 2022. 11. 16. 19:11

    이번 주에 React 상태 관리를 배우는 주간! useStore라는 Flux 패턴의 Store를 직접 만들어 사용하는 방식으로 강의가 진행되었는데, 이번 FEConf 2022에 홀맨님이 발표하신 useStore-ts의 개념을 가지고 강의에서는 조금 더 단순하게 제작을 하는 방식으로 진행되었다.

    👇 FEConf 2022 홀맨님 세션에 대해서 기록했던 10월 8일의 TIL 보러 가기

     

    10/8 TIL | 밀린 과제 처리! 그리고 useSyncExternalStore(feat. FEConf 2022)

    오늘은 8주차 주말 학습의 첫날!이지만..ㅎㅎ 7주차 퀘스트 과제를 마무리하지 못해 과제를 하였다.😅 밀린 과제는 바로 로그인/회원가입 기능 구현인데, HTTPServer로 웹 서버를 띄우고 입력 값들

    bohyunkang.tistory.com


    오늘 열린 수요 지식회에서는 아샬님이 useStore-ts 라이브러리의 활용 방법을 설명해주셨다. 참고로 useStore-ts는 React 상태 관리 라이브러리로, 자세한 설명은 아샬님의 트윗으로 대체한다! (참고로 이 트윗에서는 TypeScript 전용이라 적혀 있지만 지금은 JavaScript까지 지원한다고 한다!)

    🔗 트윗 링크: https://twitter.com/ahastudio/status/1550473632446439424

     

    🎏 전반적인 흐름

    📂 src > App.jsx

    import { useEffect } from 'react';
    
    import Posts from 'components/Posts';
    
    import usePostStore from 'hooks/usePostStore';
    
    export default function App() {
      const [, store] = usePostStore(); // 1️⃣
    
      useEffect(() => {
        store.fetchPosts(); // 2️⃣
      }, []);
    
      return (
        <Posts />
      );
    }

    1️⃣ 우선 store를 usePostStore로부터 가져온다.

    2️⃣ store에 있는 fetchPosts 메서드를 사용해 페이지 최초 렌더링 시 posts를 불러온다.

    📂 src > hooks > usePostStore.jsx

    import { useStore } from 'usestore-ts'; // 1️⃣
    
    import PostStore from 'stores/PostStore';
    
    const postStore = new PostStore();
    
    export default function usePostStore() {
      return useStore(postStore); // 2️⃣
    }

    1️⃣ & 2️⃣ useStore 라이브러리에서 제공하는 useStore를 사용하여 postStore를 만들어준다.

    📂 src > components > Posts.jsx

    import usePostStore from 'hooks/usePostStore';
    
    export default function Posts() {
      const [{ posts }] = usePostStore(); // 1️⃣
    
      if (!posts.length) {
        return (
          <p>게시물이 없습니다</p>
        );
      }
    
      return (
        <ul>
          {posts.map((post) => (
            <li key={post.id}>
              <div>
                {post.title} by {post.author}
              </div>
              <div>
                {post.body}
              </div>
            </li>
          ))}      
        </ul>
      );
    }

    1️⃣ PostStore에서 가지고 있는 posts를 가져온다.

    이때 만약 posts가 없으면 게시물이 없다는 내용을 담은 태그를 반환하고, 있으면 map을 돌아 posts를 그려준다.

    📂 src > stores > PostStore.js

    import { Store, Action } from 'usestore-ts'; // 1️⃣
    
    import { apiService } from 'services/ApiService';
    
    @Store() // 2️⃣
    class PostStore {
      posts = [];
    
      async fetchPosts() {
        const posts = await apiService.fetchPosts();
        this.setPosts(posts);
      }
    
      @Action() // 3️⃣
      setPosts(posts) {
        this.posts = posts;
      }
    }
    
    export default PostStore;

    1️⃣ useStore에서 제공하는 Store와 Action 데코레이터를 가져온다. (참고로 데코레이터는 TypeScript에서 지원하는 일종의 선언 기능으로 Java의 어노테이션이랑 비스무리한 것이라고 생각하면 된다.)

    2️⃣ 이때 PostStore 클래스에 @Store() 데코레이터로 Store임을 선언한다.

    3️⃣ 이때 setPosts메서드에 @Action() 데코레이터로 Action임을 선언한다.

    💡 다만 이때 왜 Action 데코레이터를 fetchPosts가 아닌 setPosts 메서드에 선언하였는지 의문스러울 수도 있다! 이는 fetchPosts가 비동기 함수이기 때문인데, async는 Promise 객체를 반환한다. 확실한 처리를 보장하기 위해서 setPosts에 Action 데코레이터를 선언하고 posts를 불러온 후 setPosts를 처리하는 것이다.

    추가로 데코레이터인 Store와 Action이 어떻게 구현되어 있는지 살펴보면 아래와 같다.

    📂 useStore-ts > src > decorators.ts (🔗 Github 링크)

    import { STORE_GLUE_PROPERTY_KEY } from './contants';
    
    import StoreGlue from './StoreGlue';
    
    type Klass = { new (...args: any[]): {} };
    
    export function Store() { // 1️⃣
      return function decorator<T extends Klass>(klass: T) {
        return class extends klass {
          constructor(...args: any[]) {
            super(...args);
            const glue = new StoreGlue(this);
            Reflect.set(this, STORE_GLUE_PROPERTY_KEY, glue);
            glue.update(this);
          }
        };
      };
    }
    
    export function Action() { // 2️⃣
      return (
        target: object,
        propertyKey: string,
        descriptor: PropertyDescriptor,
      ) => {
        const method = descriptor.value;
        descriptor.value = function decorator(...args: unknown[]) {
          const returnValue = method.apply(this, args);
          Reflect.get(this, STORE_GLUE_PROPERTY_KEY).update(this);
          return returnValue;
        };
      };
    }

    1️⃣ Store는 glue 인스턴스를 만들어 this의 STORE_GLUE_PROPERTY_KEY에 glue를 할당하고 glue를 업데이트하는 로직을 가지고 있다.

    2️⃣ Action은 STORE_GLUE_PROPERTY_KEY에 해당하는 프로퍼티를 가져와서 update에 this를 넣어서 실행시키는 로직을 가지고 있다.

    ⚠️ 중요! Immutable

    또한 Store의 속성은 전부 레퍼런스로 변화를 감지하기 때문에 불변성을 유지해야 한다. 

    추가로 React 18 버전에서 useExternalSync와 같이 사용하게 되면 전체적으로 화면에 보이는 것을 맞추기 위해 퍼포먼스는 조금 떨어질 수 있다고 덧붙이셨다. 그렇기 때문에 Store는 작으면 작을수록 좋다. Store를 최대한의 효율을 낼 수 있게 설계 전략을 잘 짜야한다!

    마지막 쯤에는 React 메모이제이션에 대해서도 말씀해주셨는데 이는 내일 TIL로 한번 정리해보려고 한다!(to be continued...)


    근 2주 만에 열린 수요 지식회였는데, 너무 유익했던 시간이었다! 더군다나 이번 주차 강의와 맞물려서 느껴지는 게 남달랐고, Redux 쓰면서 고통받았던 지난날이 갑자기 떠오르는데 그때에 비해 상태 관리에 대해서 내가 많이 성장했구나.. 뿌듯하면서도 동시에 이런 설계를 잘 짤 수 있으려면 어떤 노력을 해야 할지에 대해서 고민했던 시간들이었다. 근데 명쾌한 해결책은.. 결국 코딩의 신 아샬님의 흐름을 따라가는 것! 좋은 설계를 많이 보고 익히자!!! 아자아자 화이자~💪