상태를 React 안에서 관리하지 않고, 외부에서 External Store 라는 형태로 관리하는 법에 대해 알아보자

Jeong·2023년 9월 25일
0

External Store

목록 보기
1/1
post-thumbnail

키워드

  • 관심사의 분리
  • Layered Architecture
  • Flux Architecture
  • useReducer
  • useCallback

최종 목표

관심사의 분리에 따라 Store 를 사용해 상태관리를 해보자.

현재 목표

상태를 React 안에서 관리하지 않고, 외부에서 External Store 라는 형태로 관리하는 법에 대해 알아보자.

관심사의 분리란? = 내가 알빠가 뭐야

separation of concerns, 줄여서 Soc 라고 부른다.

하나의 프로그램은 작은 부품이 모여서 만들어진다. 우리는 이미 작은 컴포넌트를 합쳐서 더 큰 컴포넌트를 만드는 방식으로 개발하고 있다.

예를 들어, 다음과 같이 나타낼 수 있다. 이는 기능 을 위주로 나누었다.

  • App
    • Header
    • Main
      • Greeting
      • Counter
      • Posts
      • PostForm
        • TextField
    • Footer

이런 식으로 나누는 이유는 무엇일까?

서로의 관심사 가 다르다.

예를 들어, App 에서는 TextField 가 어떻게 동작하는지 알 필요가 없다.

각 부분은 하나의 역할, 하나의 관심사로 격리 됨으로써, 복잡도를 낮추게 된다.

관심사 분리의 관점은?

위에서 처럼 기능 관점에서 볼 수도 있고, 설계 관점, 프로세스 관점에서도 볼 수 있다.

  • 기능 관점
  • 설계 관점 (Layered Architecture)

사용자에서 가까운 것과 먼 것으로 구분한다.

예를 들어,
가장 가까운 건 UI 를 다루는 부분,
그 다음에는 Business 를 다루는 부분,
그 너머에는 데이터에 접근하고 저장하는 부분으로 나눌 수 있다.

  • 프로세스 관점

Input → Process → Output

거대한 프로그램이 아니라고 해도, 흔히 이렇게 3단계로 코드를 적절히 구분만 하면,
코드를 이해하고 유지보수하는데 크게 도움이 된다.

하나의 Output은 다시 사용자에게 Input을 요청하게 되고, 일반적인 프로그램은 다음과 같이 계속 순환하는 구조가 된다.

  1. Input: 프로그램 시작
  2. Process: 프로그램 초기화
  3. Output: 사용자에게 초기 UI 보여주기
  4. Input: 사용자의 입력
  5. Process: 사용자의 입력에 따라 처리
  6. Output: 처리 결과 보여주기
  7. Input: 사용자의 또 다른 입력
  8. …반복…

만약 Input Process 가 묶여 있는 구조라고 생각해보자. Process 부분을 테스트 하고 싶은데, I/O 가 섞여 있으면 복잡해진다. Process 부분이 따로 나와 있으면, 단위 테스트를 할 수 있어서 테스트 하기가 쉬워진다.

(물론 E2E 테스트를 배웠기 때문에 I/O 테스트도 가능하다.)

Flux Architecture

MVC 에 대한 대안이다.

단방향 데이터 Flow 를 강조한다.

  1. Action
    이벤트/메시지 같은 객체이다.
  2. Dispatcher
    (여러) Store로 Action을 전달한다. 메시지 브로커와 유사하다.
  3. Store (여러 개)
    받은 Action에 따라 상태를 변경한다. Store 를 구독하고 있는 View 에게 상태 변경을 알린다.
  4. View
    Store의 상태를 반영한다.

Redux

이것을 배경으로 하는 Redux는 단일 Store를 사용함으로써
좀 더 단순한 그림을 제안한다.

  1. Action
  2. Store
    Store의 dispatch를 통해 Action을 받고,
    사용자가 정의한 reducer를 통해 State를 변경한다.
  3. View
    State를 반영한다.

여기서 State를 변경한다 의 의미는
기존의 State 를 고치지 않고, 새로운 State 를 만든다는 것이다.

const state = { name: 'tester' }

→ state.name = 'New Name' (X)const nextState = { ...state, name: 'New Name' } (O)

3단계를 거칠게 매칭해보자.

  • Input → Action + dispatch
  • Process → reducer (사용자가 정의한 reducer를 통해 State를 변경한다.)
  • Output → View (React 가 상태를 UI 에 반영한다.)

External Store 란?

지금까지는 상태를 useState 같은 Hook 을 이용해서 처리했다. 이제 다르게 처리해보자.

먼저 External Store 의 뜻은 무엇일까?

External: React 의 바깥 부분을 의미한다.
→ External Store: Store 가 React 의 안에 있지 않다 를 의미한다.

Architecture 관점에서 말하는 바깥 부분 이 아니다.
따지고 보면 가장 바깥 부분은 UI 이다. 즉, React 가 바깥 부분에 있다.
여기서는 말하는 바깥 부분 은 React 입장에서 바깥 부분 을 말한 것이다.

상태 관리

useState 로 상태 관리하기

Increase 버튼을 누르면 count 가 1씩 증가한다. 화면에도 증가되는 상태 값이 보인다.

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button type="button" onClick={handleClick}>
        Increase
      </button>
    </div>
  );
}

useState 없이 상태 관리하기: 문제 발생

다음과 같이 작성하면 상태가 바뀌어도 화면에 적용되지 않는 문제가 발생한다.

import { useState } from 'react';

let count = 0;

export default function Counter() {

  const handleClick = () => {
    count += 1;

    console.log(count);
  };

  return (
    <div>
      <p>{count}</p>
      <button type="button" onClick={handleClick}>
        Increase
      </button>
    </div>
  );
}

useState 없이 상태 관리하기: forceUpdate

Class 컴포넌트를 쓰던 시절에는 이렇게 사용했다. 강제로 리렌더링을 했다.

import { useReducer } from 'react';

let count = 0;

export default function Counter() {
  const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);

  const handleClick = () => {
    count += 1;

    // 강제로 렌더링
    forceUpdate();
  };

  return (
    <div>
      <p>{count}</p>
      <button type="button" onClick={handleClick}>
        Increase
      </button>
    </div>
  );
}

Custom Hook 으로 분리

Custom Hook 으로 분리했더니, 상태를 React 가 관리하지 않게 됐다.

이런 식으로 만드는게 External Store 의 기본적인 아이디어이다.

import { useState } from 'react';

let count = 0;

function useForceUpdate() {
  const [state, setState] = useState(0);

  const forceUpdate = () => {
    setState(state + 1);
  };

  return forceUpdate;
}

export default function Counter() {
  const forceUpdate = useForceUpdate();

  const handleClick = () => {
    count += 1;

    forceUpdate();
  };

  return (
    <div>
      <p>{count}</p>
      <button type="button" onClick={handleClick}>
        Increase
      </button>
    </div>
  );
}

함수가 항상 같게 된다, useCallback

함수를 항상 같게 만들 수 있다.

왜 함수를 같게 만들어야 할까?

나중에 useEffect(무언가를 처리, [forceUpdate]); 를 해주기 위해서이다.
forceUpdate 가 바뀌가 되었을 때 무언가를 처리해주기 위해서이다.

// hooks/useForceUpdate.ts
import { useState, useCallback } from 'react';

export default function useforceUpdate() {
  const [, setState] = useState({});
  return useCallback(() => setState({}), []);
}
// components/Counter.tsx
import useForceUpdate from '../hooks/useForceUpdate';

let count = 0;

export default function Counter() {
  const forceUpdate = useForceUpdate();

  const handleClick = () => {
    count += 1;

    forceUpdate();
  };

  return (
    <div>
      <p>{count}</p>
      <button type="button" onClick={handleClick}>
        Increase
      </button>
    </div>
  );
}

결론

이런 접근을 잘 하면, React 가 UI 를 담당하고, 순수한 TypeScript (또는 JavaScript) 가 비즈니스 로직을 담당하는, 관심사의 분리(Separation of Concerns) 를 명확히 할 수 있다.

자주 바뀌는 UI 요소에 대한 테스트 대신, 오래 유지되는 (바뀌면 치명적인) 비즈니스 로직에 대한 테스트 코드를 작성해 유지보수에 도움이 되는 테스트 코드를 치밀하게 작성할 수 있다.

다음에는

Store 라는 개념을 사용해보자.

TSyringe 와 TSyringe로 구축된 Store 를 이용해서 Redux 를 만들어보자.

다음에는 관심사의 분리가 왜 좋은지 더 와닿을 것이다.

profile
성장중입니다 🔥 / 나무위키처럼 끊임없이 글이 수정됩니다!

0개의 댓글