> For the complete documentation index, see [llms.txt](https://toktokhan.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://toktokhan.gitbook.io/docs/basic-guides/convention/contexts/information.md).

# Boilerplate

## Global Context

전역적으로 관리되는 컨텍스트 입니다.

state 와 handler, 그리고 사용되는 web-storage 등의 값이 포함 되어 있습니다.

## web storage

web-storage 는 자주 사용되는 api 중에 하나입니다.

하지만, localStorage 는 리엑트에서 다양한 불편함을 가지고 있는데, 아래와 같습니다.

* 데이터에 접근할 때 JSON.parse 와 stringfy 가 수반되어야 합니다.
* 데이터의 수정사항을 react 의 생명주기에서 감지 할 수 없습니다.(데이터 수정으로 리랜더링이 촉발되지 않습니다.)

위와 같은 불편함을 해결하고자 만들어진 hook 이 useSyncWebStorage 입니다.

전역 컨텍스트에서는 위 훅을 사용해 아래 처럼 사용하고 있습니다.

#### 사용 방법

{% tabs %}
{% tab title="token storage 선언하기" %}

```tsx
// src/utils/web-storage/todo.ts
import { SyncedStorageFactory } from './helper/synced-storage-factory';

export type TodoType = {
  text: string;
};

export const {
  connector: todoConnector, //
  storage: todoStorage,
} = SyncedStorageFactory.createLocal<TodoType[]>('todo');

```

{% endtab %}

{% tab title="web storage 선언하기" %}

```tsx
// src/contexts/global/hooks/useWebStorage.ts

import { useSyncWebStorage } from '@/hooks/useSyncWebStorage';

import { todoConnector } from '@/utils/web-storage/todo';

export const useWebStorage = () => {
  const todoList = useSyncWebStorage(todoConnector);

  return { token, todoList };
};
```

{% endtab %}

{% tab title="context 에서 내려주기" %}

```tsx
// src/contexts/global/useGlobalStoreContext.ts

import { useWebStorage } from './hooks/useWebStorage';

const useGlobalStore = () => {
  const { state, dispatch } = useGlobalState();
  const webStorage = useWebStorage()

  return { dispatch, state, webStorage };
};

```

{% endtab %}

{% tab title="수정 및 조회하기" %}

```tsx

todoStorage?.set([]); // 컴포넌트 바깥에서도 수정이 가능합니다.
  
function Home() {
  // 조회
  const todoList = useGlobalContext((ctx) => ctx.webStorage.todoList);
  // 수정
  const addTodo = (todo: TodoType) => { 
    const prev = todoList || [];
    todoStorage?.set([...prev, todo]);
  };

  return (
    <Box>
      <Button onClick={() => addTodo({ text: 'new' })}>Add</Button>
      <List>
        {todoList?.map((todo) => (
          <ListItem>{todo.text}</ListItem>
        ))}
      </List>
    </Box>
  );
}
```

{% endtab %}
{% endtabs %}

#### + useSyncWebStorage 는 어떻게 구현 되었나요?

useSyncWebStorage 를 사용하기 위해선 3개의 모듈이 필요합니다.

* `useSyncExternalStore`:  react 18 에 나온 hook 이며 구독, 알림 패턴을 통해 외부 상태와 리액트 생명주기를 연결시켜주는 훅입니다. 여기서 해당 hook 은 구독을 하는 입장에 해당합니다.
* `Storage`: 위의 훅에 연결될 Storage 주체입니다 useSyncExternalStore 에게 알림을 수행하는 입장에 해당합니다.
* `Connector`:  알림을 관리하고, useSyncExternalStore 와 Storage 를 연결 시켜주는 역할입니다.

{% tabs %}
{% tab title="useSyncExternalStore" %}

### useSyncExternalStore

useSyncExternalStore 은 크게 2가지 함수를 사용자에게 전달받아 구독 기능을 수행합니다. 다음은 useSyncExternalStore 입장에서 구독기능을 수행하는 방법에 대해 설명합니다.

#### subscribe

인자로 받는 subscribe 함수를 통하여 listener 함수를 인자로 사용자에게 전달해 줍니다. listerner 함수는 구독하고 있는 모듈에게 알림을 주는 '알림 함수' 에 해당하며, 함수가 실행 될시, 리랜더링을 수행합니다.

subscribe 함수의 return 값으로 '정리 함수' 를 전달받습니다. listener 를 통해 리랜더링이 되고 나면, 정리 함수를 실행시키고, 사용자에게 새로운 알림 함수를 전달해 줍니다.

#### getSnapShot

listener 함수가 실행되서 리랜더링이 발생한 시점의 랜더링 할 데이터를 사용자에게 전달 받습니다.

#### +getServerSnapShot

server-side 시점의 데이터를 사용자에게 전달 받습니다

**exmple**

```tsx
// example

type Listener = () => void
let myListenr: Listener | null = null

let count = 0
const increase = () => { 
    count += 1;
    if (myListener) myListener()
}

const subscribe = (listener: Listener) => {
    myListener = listener;    
    return () => { myListener = null } // clean-up 함수
}

const getSnapShot = () => count

const Component = () => {
    const count = useSyncExternalStore(subscribe, getSnapShot);
    return (
        <div>
            <button onClick={increase}>increase</button>
            <p>{count}</p> 
        </div>
    )
}
```

#### 참고

* 공식문서: <https://react-ko.dev/reference/react/useSyncExternalStore>&#x20;
* useSyncExternalStore 구현 예시:  <https://blog.saeloun.com/2021/12/30/react-18-useSyncExternalStore-api/#understanding-usesyncexternalstore-hook>

### 우리 프로젝트에서는?

useSyncExternalStore 로부터 전달받은 알림함수를 관리하는 connector 모듈과 함께 사용을 합니다.

```tsx
// src/hooks/useSyncWebStorage.ts
import { useSyncExternalStore } from 'react';

import { ReactSyncConnector } from '@/utils/react/react-sync-connector';

export const useSyncWebStorage = <T>(connector: ReactSyncConnector<T>) => {
  return useSyncExternalStore(
    connector.subscribe, 
    connector.getSnapshot,
    connector.getServerSnapShot,
  );
};
```

{% endtab %}

{% tab title="ReactSyncConnector" %}

### ReactSyncConnector

알림함수를 관리하고, Storage 와 useSyncExternalStore 을 연결 하는 모듈입니다.

useSyncExternalStore 로 부터 리랜더링을 촉발시키는 알림함수 를 받아 관리하고,

Storage 모듈에 알림 함수를 넘겨주어 useSyncExternalStore 과 연결시켜주는 역할을 합니다.

```tsx

import { ReactSynced } from './react-synced';

export class ReactSyncConnector<Data> {
  listeners: Array<() => void> = [];
  constructor(private synced: ReactSynced<Data> | null) {
    // storage 모듈에 해당합니다.
    this.synced = synced;
    // storage 모듈에 알림함수를 실행시키는 emitChange 를 넘겨줍니다.
    this.synced?.connect(this.emitChange);
  }

 // useSyncExternalStore 에서 알림함수를 받고, 저장해둡니다. useSyncExternalStore 에게 정리함수를 넘겨줍니다.
  subscribe = (listener: () => void) => {
    if (this.synced) this.listeners = [...this.listeners, listener];
    return () => {
      this.listeners = this.listeners.filter((l) => l !== listener);
    };
  };

 // 알림함수가 실행되서, 리랜더링 될 시 조회할 데이터를 넘겨줍니다.
  getSnapshot = () => {
    return this.synced?.data ?? null;
  };

  getServerSnapShot = () => {
    return null;
  };

  // 알림함수를 실행시킵니다. (구독 모듈에 알림이 전달되며, 리랜더링 됩니다.).
  emitChange = () => {
    for (const listener of this.listeners) {
      listener();
    }
  };
}

```

{% endtab %}

{% tab title="SyncedStorage" %}

#### React Synced

Storage 가 상속 받는 부모 Class 입니다. Connector 와 연결되어 알림함수를 받고, data 를 수정 할때 마다. 알림함수(리랜더링) 을 실행시키는 역할을 담당합니다.

```typescript

export class ReactSynced<T> {
  private _data: T | null = null;
  public listener: (() => void) | null = null;

  get data(): T | null {
    return this._data;
  }

 // data 가 set 될때 마다. 알림함수를 실행시켜 리랜더링을 촉발합니다.
  set data(data: T | null) {
    this._data = data;
    this.listener?.();
  }
  // connector 에서 리랜더링을 촉발시키는 알림함수 (emitChange) 를 받습니다.
  connect = (listener: () => void) => {
    this.listener = listener;
  };

  unConnect = () => {
    this.listener = null;
  };
}
```

#### SyncedStorage

storage 모듈의 주체입니다.\
web storage 의 메소드를 사용하여 데이터를 관리하는 역할을 합니다.

key 와 data type, storage를 받음으로써 특정 storage 의, key 별 데이터를 타입정의와 함께 관리할 수 있습니다.

react synced 를 상속하기때문에, `this.data = ...`과 같은 setter 실행시, 알림함수(리랜더링)가 실행 됩니다.

```tsx
 import { ReactSynced } from '@/utils/react/react-synced';

export class SyncedStorage<Data> extends ReactSynced<Data> {
  constructor(public key: string, private storage: Storage) {
    super();
    this.key = key; 
    this.storage = storage;

    // 클래스 생성시 data(getSnapshot이 바라보고 있는 값에)에 현재 storage 의 data 를 할당해줍니다.
    this.data = this.get(); 
  }

  get = (): Data | null => {
    if (this.data !== null) return this.data;
    const item = this.storage.getItem(this.key);
    if (item === null) return null;
    return JSON.parse(item);
  };

  set = (data: Data | null): void => {
    this.data = data;
    this.storage.setItem(this.key, JSON.stringify(data));
  };

  remove = (): void => {
    this.data = null;
    this.storage.removeItem(this.key);
  };
}

```

{% endtab %}

{% tab title="SyncedStorageFactory" %}
앞에서 소개했던   `ReactSyncConnector` 와 `SyncedStorage`, 을 `useSyncExternalStore` 사용하기 쉽게 한곳에서 connector 과 storage 를 동시에 만들어 주는 역할을 담당합니다.

```tsx
import { ReactSyncConnector } from '@/utils/react/react-sync-connector';

import { SyncedStorage } from './synced-storage';

export class SyncedStorageFactory {
  static createLocal = <Data>(key: string) => {
    const store = typeof window === 'undefined' ? null : localStorage;
    return this.create<Data>(key, store); //
  };

  static createSession = <Data>(key: string) => {
    const store = typeof window === 'undefined' ? null : sessionStorage;
    return this.create<Data>(key, store);
  };

  static create = <Data>(key: string, store: Storage | null) => {
    const storage = store ? new SyncedStorage<Data>(key, store) : null;
    const connector = new ReactSyncConnector(storage);

    return { storage, connector };
  };
}
```

{% endtab %}
{% endtabs %}

## useGlobalHandler, useGlobalState

전역 context 에는 위와 같은 훅이  존재하는데요. 이는 비즈니스로직의 추상화를 권장하기 위해 만들어진 hook 입니다. 사용되는 범위로 훅을 분리하는것도 좋지만, 주제별 state 와 logic 을 가지고 있는 hooks 를 만드는것도 좋은 방법입니다.

### example

**범위로 구분하기**

* useGlobalContext
  * useGlobalState: 앱 전역에서 사용되는 state 와 state setter 로직
  * useGlobalHandler: 앱 전역에서 사용되는 handler 로직

**주제로 구분하기**

* useGlobalContext
  * useLogin
  * useToast

<br>
