# External Store

## External Store

> 리액트 외부에 Store(저장소)를 두고 상태를 관리함으로써, 상태를 제어하는 관심사와 UI(컴포넌트)의 관심사를 분리

&#x20;React 컴포넌트 입장에서는 “전역”처럼 여겨진다.

&#x20;**“Prop Drilling” 문제를 우아하게 해결**할 수 있는 방법 중 하나(React로 한정하면 **Context도 쓸 수 있다**).

{% hint style="info" %}
External → (React) 외부를 의미한다.

일반적인 아키텍처에서는 UI(React)가 가장 바깥쪽이지만 여기서 말하는 Extenral은 안이냐 밖이냐라는 관점이 아니라 React 입장에서 외부를 의미
{% endhint %}

### forceUpdate

React를 사용하는 것이 아닌 React 외부를 통해 상태관리를 하는 것이므로  useState를 사용하여 상태관리를 하는것이 아니다.&#x20;

그래서 화면을 리렌더링 시킬 방법이 필요한데 아래와 같이 강제적으로 리렌더링하는 방식을 사용한다.

```typescript
function useForceUpdate() {
  const [, setState] = useState({});
  return useCallback(() => setState({}), []);
}
```

### [React 측 forceUpdate에 대한 입장](https://ko.reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate)

React 공식문서에 나와있는 forceUpdate 코드

가능하면 해당 패턴을 피하라고 주의를 주고 있다.

```tsx
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
  forceUpdate();
}
```

#### useReducer

> React에서 제공하는 상태 관리 훅이며, useState와 유사하지만 상태 업데이트 로직을 컴포넌트 외부로 분리할 수 있다.

useReducer가 기본형이고 useState는 내부적으로 useReudcer를 사용하는 방식이다.

여러 컴포넌트에서 공유되는 상태를 관리할 때 도움이 된다.

우리가 흔히 알고있는 Redux의 그것과 비슷함

```tsx
const [state, dispatch] = useReducer(reducer, initialState);
```

* **dispatch**: 액션을 보내는 함수를 반환
* **reducer**: 상태 업데이트 로직을 처리하는 함수

{% hint style="info" %}
리엑트 내부적으로 사용하는 상태관리(useState, useReduer, props, context)를 **Internal Store**라고 한다.&#x20;
{% endhint %}

#### useCallback

> React에서 제공하는 성능 최적화를 위한 훅이며, 성능 최적화를 위해 함수를 캐시하고 재사용한다.

매 렌더링 마다 새로운 함수를 생성하지 않고 이전에 생성된 함수를 재사용하도록 보장함

자식컴포넌트에 props로 콜백함수를 전달할 때 props의 변화로 불필요하게 리렌더링 되는 것을 방지하기 위해 사용된다.

useCallback만 사용한다고 해서 최적화 되는 것이 아니라 또 다른 성능 최적화 훅인 `React.memo()` 와 같이 사용해야 최적화 된다는 것에 유의

{% hint style="info" %}
**React.memo Hooks**

이전에 렌더링된 결과를 캐시하고, 다음 렌더링 시 현재 props와 얕게 비교(변경가능)해서 같다면 리렌더링을 하지 않고 캐시된 결과를 사용하는 식으로 리렌더링을 방지한다.
{% endhint %}

```tsx
const callbackFn = useCallback(() => {
  doSomething
}, [dependency])
```

useEffect의 의존성 배열과 비슷한데 함수 내부에서 참조하는 상태나 props 중에서 변경되는 것이 있을때 새로운 함수를 생성하도록 지정한다.

## Props Drilling

> 부모에서 React 트리의 모든 중첩된 자식에게 데이터를 전달하는 것을 의미한다.

아래 코드와 같이 중간 단계의 컴포넌트가 props를 단순히 자식에게 전달하기만 하는 형태를 **props drilling**이라 함

```tsx
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Children count={count} setCount={setCount} />
    </div>
  )    
}

function Children({count, setCount}) {
  return (
    <div>
      <ChildrenChildren count={count} setCount={setCount} />
    </div>
  )    
}

function ChildrenChildren({count, setCount}) {
  return (
    <div>
      <p>{`count: ${count}`}</p>
      <button type="button" onClick={() => setCount((prev) => prev + 1)}>Increase</button>
    </div>
  )    
}
```

### Props Drilling의 문제점

* 컴포넌트 구조가 변경되면 props를 전달하는 모든 컴포넌트를 수정해야 하므로 번거롭다.
* 코드가 금방 복잡해지고 실수하기 쉽다.

## Context API

> React에서 제공해주는 Context API Hooks로 Props Drilling을 최소화할 수 있다.

<figure><img src="https://1912740209-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F3GVYdZvmqQyBVq29ebqk%2Fuploads%2FlyHP42P96Sb4rKTCCSDL%2Fcontext.png?alt=media&#x26;token=5609bfda-3f6e-45b5-af82-48757e4e5187" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Context의 사용 목적은 복잡하게 "중첩 된" 하위 컴포넌트들에 데이터를 공유하는 것

Context를 사용하면 컴포넌트 재사용이 어려우므로 꼭 필요한 경우에만 사용해야 한다.
{% endhint %}

```tsx

import {createContext, useContext, useState, useMemo} from 'react';

interface CounterContextValue {
  count: number;
  setCount: () => void;
}

export const CounterContext = createContext({} as CounterContextValue);

export function useCounter() {
  const contextValue = useContext(CounterContext);
  
  if (!contextValue) {
    throw new Error('useCounter must be used within CounterProvider');
  }
  
  return contextValue;
}

export default CounterProvider({children}: React.PropsWithChildren) {
  const [count, setCount] = useState(0);
  
  const value = useMemo(() => ({count, setCount}), [count, setCount])
  
  return (
    <CounterContext.Provider value={value}>
      {children}
    </CounterContxt.Provider>
  )
}
```

### Context 적용 코드

> 넘겨주는 Props들을 모두 제거하니 깔끔해졌다.

```tsx
function Parent() {
  return (
    <CounterProvider>
      <div>
        <Children/>
      </div>
    </CounterProvider>
  )    
}

function Children() {
  return (
    <div>
      <ChildrenChildren/>
    </div>
  )    
}

function ChildrenChildren() {
  const {count, setCount} = useCounter();
  
  return (
    <div>
      <p>{`count: ${count}`}</p>
      <button type="button" onClick={() => setCount((prev) => prev + 1)}>Increase</button>
    </div>
  )    
}
```

{% hint style="danger" %}
Context API + useReduecr 조합으로 상태관리를 흉내낼수 있지만 관리되는 컨텍스트 값 중 일부가 변경이 되었을 때 변경되지 않은 다른 상태값에 연결된 컴포넌트 또한 리렌더링이 발생하는 Side Effect가 있다.&#x20;
{% endhint %}

## External Store 직접 구현해보기 with Tsyringe

> 여러 스토어를 통해 관리되는 extenral store를 직접 구현 해보자.

### ObjectStore.

> 여러 스토어의 베이스가 되는 부모 클래스

```typescript
type Listener = () => void;

export default abstract class ObjectStore {
  private listeners = new Set<Listener>();

  protected publish() {
    this.listeners.forEach((listener) => listener());
  }

  addListener(listener: Listener) {
    this.listeners.add(listener);
  }

  removeListener(listener: Listener) {
    this.listeners.delete(listener);
  }
}
```

### CounterStore

```typescript
import { singleton } from 'tsyringe';

import ObjectStore from './objectStore';

@singleton()
export default class CounterStore extends ObjectStore {
  counter = 0;

  increase(step = 1) {
    this.counter += step;
    this.publish();
  }

  decrease(step = 1) {
    this.counter -= step;
    this.publish();
  }
}
```

### useObjectStore

```typescript
import { useEffect } from 'react';

import ObjectStore from '../stores/objectStore';

import useForceUpdate from './useForceUpdate';

export default function useObjectStore<T extends ObjectStore>(store: T): T {
  const forceUpdate = useForceUpdate();

  useEffect(() => {
    store.addListener(forceUpdate);

    return () => {
      store.removeListener(forceUpdate);
    };
  }, [store, forceUpdate]);

  return store;
}
```

### useCounterStore

```typescript
import { container } from 'tsyringe';

import CounterStore from '../stores/counterStore';

import useObjectStore from './useObjectStore';

export default function useCounterStore() {
  const store = container.resolve(CounterStore);

  return useObjectStore(store);
}

```

### Usage

`CounterController.tsx`

```tsx
import useCounterStore from '../hooks/useCounterStore';

export default function CounterController() {
  const store = useCounterStore();

  const handleClickIncrease = (step?: number) => () => {
    store.increase(step);
  };

  const handleClickDecrease = (step?: number) => () => {
    store.decrease(step);
  };

  return (
    <div>
      <button type="button" onClick={handleClickIncrease(10)}>
        Increase 10
      </button>
      <button type="button" onClick={handleClickIncrease()}>
        Increase
      </button>
      <button type="button" onClick={handleClickDecrease(10)}>
        Decrease 10
      </button>
      <button type="button" onClick={handleClickDecrease()}>
        Decrease
      </button>
    </div>
  );
}
```

`Counter.tsx`

```tsx
import useCounterStore from '../hooks/useCounterStore';

export default function Counter() {
  const { counter } = useCounterStore();

  return (
    <div>
      <p>{`counter: ${counter}`}</p>
    </div>
  );
}
```

{% hint style="success" %}
이런 접근을 잘 하면, React가 UI를 담당하고, 순수한 TypeScript(또는 JavaScript)가 비즈니스 로직을 담당하는, 관심사의 분리(Separation of Concerns)를 명확히 할 수 있다. 자주 바뀌는 UI 요소에 대한 테스트 대신, 오래 유지되는(바뀌면 치명적인) 비즈니스 로직에 대한 테스트 코드를 작성해 유지보수에 도움이 되는 테스트 코드를 치밀하게 작성할 수 있다.
{% endhint %}
