저는 React의 외부에서 External Store 형태로 데이터를 관리했습니다.
External Store로 관리한 이유는 '관심사의 분리' 때문입니다.
React 입장에서 데이터가 어떻게 관리되는지 알 필요가 없습니다.
React의 useState 없이 상태를 관리해야 합니다.
React는 '상태가 바뀌면' 상태가 업데이트 되었습니다.
이젠 React의 도움을 못 받으니 어떻게 하면 좋을까요?
저는 TSyringe를 이용했습니다. 이는 의존성을 주입해주는 역할을 합니다. External Store를 관리하는데 활용할 수 있습니다.
React 컴포넌트 입장에서는 전역처럼 여겨집니다. (전역변수를 쓰는 것처럼 느껴지는데, 전역변수는 아닙니다)
그래서 Prop Drilling 문제를 해결할 수 있는 방법 중 하나입니다.
참고로 TSyringe말고 다른 방법도 있습니다. 예를 들어, Context가 있습니다. 하지만 Context는 전체를 바꾸기 때문에 비효율적이라서 선택하지 않았습니다.
reflect-metadata는 polyfill입니다.
polyfill은 더 이상 지원하지 않는 곳에서도 지원을 가능하게 하는 역할을 의미합니다.
그래서 reflect-metadata는 더 이상 TypeScript에서 데코레이터와 메타데이터를 지원하지 않는 곳에서도 지원이 가능하게 하는 역할을 합니다.
npm i tsyringe reflect-metadata
reflect-metadata는 모든 프로그램이 시작하는 위치에 써줍니다. 저는 main.tsx에 썼습니다.
import 'reflect-metadata';
테스트 코드를 작성하셨다면, reflect-metadata를 테스트 쪽에도 추가해야 합니다.
테스트는 시작이라고 할 만한 위치가 없어서, 저는 jest.config.js에 작성된 setuptests.ts 파일의 상단에 작성했습니다.
데코레이터를 허용하기 위해서 tsconfig.json 파일에서 아래 두개의 주석을 해제합니다.
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
usestore-ts는 필수가 아닙니다. usestore-ts 없이도 코드를 작성할 수 있습니다.
하지만 아래 예제는 usestore-ts를 사용했습니다.
usestore-ts는 시드웨일 라이브러리입니다.
React가 상태를 변경하면 알아서 업데이트 하는 것처럼, 우리도 상태가 바뀌면 연결된 컴포넌트를 forceUpdate해야 합니다. 이를 publish(상태 변경 알림)이라고 하겠습니다.
usestore-ts를 사용한 이유는 데이터를 publish가 편해서입니다.
이전에는 따로 BaseStore를 만들어서 해당 Store에 데이터를 publish하는 부분을 넣고, 모든 Store가 BaseStore를 상속받게 했습니다. 그리고 데이터의 값이 변경되면 BaseStore의 publish를 실행했습니다.
하지만 usestore-ts의 Action 데코레이터를 쓰면 publish를 따로 해줄 필요가 없습니다.
내부에서 자동으로 해줍니다.
npm install usestore-ts
Store를 만드는데 마치 전역인 것처럼 동작합니다.
singleton은 전역에서 하나라는 의미입니다.
tsyringe는 Ioc Container를 제공합니다. 그래서 Ioc Container가 객체 생성을 알아서 해줍니다.
Store가 다른 것에 의존성이 있다면 그것까지 알아서 조립해줍니다.
// src/store/LikeStore.ts
import { singleton } from 'tsyringe';
import { Action, Store } from 'usestore-ts';
import { apiService } from '../services/ApiService';
@singleton()
@Store()
export class LikeStore {
id = '';
liked = 0;
error = false;
@Action()
increase(id : string) {
this.id = id;
this.liked += 1;
}
@Action()
setError() {
this.error = true;
}
}
이렇게 커스텀 Hook으로 분리하는 이유는
React가 UI를 담당하고, 순수한 TypeScript는 비지니스 로직을 담당하기 위해서 입니다.
이를 통해 '관심사의 분리'를 명확히 할 수 있습니다.
// src/hooks/useLikeStore.ts
import { container } from 'tsyringe';
import { useStore } from 'usestore-ts';
import { LikeStore } from '../store/LikeStore';
export default function useLikeStore() {
const store = container.resolve(LikeStore);
return useStore(store);
}
// src/components/MapDetail/DetailedMapPage.tsx
import useLikeStore from '../../hooks/useLikeStore';
export default function DetailedMapPage() {
const [, store] = useLikeStore();
const handleBoothLike = (value: string) => {
store.increase(value);
};
return (
<button
type="button"
onClick={() => handleBoothLike(id)}
>
무한 좋아요
</button>
);
};
// src/services/ApiService.ts
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_URL;
export default class ApiService {
private instance = axios.create({
baseURL: API_BASE_URL,
});
async fetchLike({ id, liked }:{
id: string;
liked: number;
}): Promise<{ liked: number; }> {
const response = await this.instance.put(`booth/liked/${id}`, { likeCount: liked });
return response.data;
}
}
export const apiService = new ApiService();
TSyringe를 통해서 데이터를 전역처럼 관리할 수 있었고,
usestore-ts를 통해서 상태 변경 알림을 쉽게 처리할 수 있었습니다.
그리고 이렇게 데이터를 React의 외부에서 관리하는 이유는
관심사의 분리를 위해서였습니다.
데이터를 React의 외부에서 관리해보며, 선언형 UI가 얼마나 편했는지 알 수 있게 됐습니다.