HAND-PYEON / handpyeon-web

핸드편, 내 손안의 편의점 할인 정보
0 stars 2 forks source link

프로젝트 아키텍쳐 구조 설계 #13

Open Ssoon-m opened 1 year ago

Ssoon-m commented 1 year ago

아키텍쳐 구조

스크린샷 2023-06-18 오후 10 31 21

다음과 같이 구조를 잡을 예정입니다.

view

화면에 보여질 부분을 담당하는 레이어입니다. view부분은 page가 될 수도 있고, react component가 될 수도 있습니다.(단, 공통 컴포넌트 부분은 제외 합니다)

viewmodel

view를 그리기 위한 데이터가 존재하는 레이어입니다. viewmodel은 view에서의 이벤트를 감지해서 repository를 업데이트하고 업데이트된 결과를 다시 view에 전달합니다. 이 역할을 react-query가 담당하게 됩니다.

repository

서버 데이터에 접근하기 위한 레이어입니다.

infrastructure

외부 관련 모듈이 있는 레이어입니다. http 통신이나 웹 스토리지 접근등을 담당합니다.

💡viewmodel 의존성에 대한 고민

기존에 생각한 구조

아래 코드를 보면 todo component에서 useGetTodoQuery(viewmodel)를 바로 호출하고 있습니다. 이렇게 사용할 경우 장단점은 아래와 같습니다.

장점

단점

// src/hooks/query

//...
const todoRepository = TodoRepository(Http);

export const useGetTodoQuery = () => {
  return useQuery({
    queryKey: ['todo'],
    queryFn: () => todoRepository.getList(),
    suspense: true,
  });
};
// src/components/todo

//...
const TodoList = () => {
  const { data } = useGetTodoQuery();
  return (
    <ul>
      {data?.map((todo) => (
        <TodoItem
          key={todo.id}
          title={todo.title}
          contents={todo.contents}
        ></TodoItem>
      ))}
    </ul>
  );
};

고민중인 구조

viewmodel에 repository 의존성을 주입해서 사용합니다. 이렇게 사용할 경우 장단점은 아래와 같습니다.

장점

단점

// src/hooks/query

//...
import TodoRepository from "@/repository/TodoRepository";

const TodoViewModel = (todoRepository: ReturnType<typeof TodoRepository>) => {
  const useGetTodoQuery = () => {
    return useQuery({
      queryKey: ['todo'],
      queryFn: () => todoRepository.getList(),
      suspense: true,
    });
  };
  return { useGetTodoQuery };
};
export default TodoViewModel;
// src/components/todo

//...
import TodoRepository from "@/repository/TodoRepository";
import Http from "@/infrastructures/client";

const todoRepository = TodoRepository(Http);

const TodoList = () => {
  const { data } = TodoViewModel(todoRepository).useGetTodoQuery();
  return (
    <ul>
      {data?.map((todo) => (
        <TodoItem
          key={todo.id}
          title={todo.title}
          contents={todo.contents}
        ></TodoItem>
      ))}
    </ul>
  );
};

테스트 예시

모킹 라이브러리의 도움 없이 테스트 코드를 쉽게 작성 가능

const mockedData = {
  todoList: [
    { id: 1, title: "Todo 1", contents: "Todo 1 contents" },
    { id: 2, title: "Todo 2", contents: "Todo 2 contents" },
  ],
};

const http: IHttp = {
  async get<T>(url: string) {
    const data: T = mockedData as any;
    return { data };
  },
  async post<T>(url: string) {
    const data: T = true as any;
    return { data };
  },
};

const todoRepository = TodoRepository(http);

const queryClient = new QueryClient();

const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe("TodoViewModel", () => {
  test("returns the mocked data from useGetTodoQuery", async () => {
    const { result } = renderHook(
      () => TodoViewModel(todoRepository).useGetTodoQuery(),
      {
        wrapper,
      }
    );

    await waitFor(() => result.current.isSuccess);

    expect(result.current.data).toEqual(mockedData.todoList);
  });
});
brightchul commented 1 year ago

MVVM과 클린 아키텍쳐를 섞은 느낌이 조금 나는거 같습니다. 보다가 몇가지 의문점이 있어서 코멘트 드립니다.

  1. 웹 클라이언트에는 DB가 없는데 첨부하신 도표에는 repository가 DB안에 있어서 조금 혼란이 오네요. 서버데이터가 DB라는 의미일까요?
  2. MVVM에서 Model은 어떤 부분에서 담당하는지 알려주세요.
  3. 전역 상태 스토어는 설명하신 부분중에 담당하는 부분이 있는지 아닌지 알려주세요.
Ssoon-m commented 1 year ago

MVVM과 클린 아키텍쳐를 섞은 느낌이 조금 나는거 같습니다. 보다가 몇가지 의문점이 있어서 코멘트 드립니다.

  1. 웹 클라이언트에는 DB가 없는데 첨부하신 도표에는 repository가 DB안에 있어서 조금 혼란이 오네요. 서버데이터가 DB라는 의미일까요?
  2. MVVM에서 Model은 어떤 부분에서 담당하는지 알려주세요.
  3. 전역 상태 스토어는 설명하신 부분중에 담당하는 부분이 있는지 아닌지 알려주세요.

우선 해당 이슈는 mvvm 패턴을 프로젝트에 어떤식으로 적용할지에 대한 내용이였는데 이 부분에 대한 설명을 제대로 하지 않은거 같아서 먼저 말씀 드립니다! store와 util등의 폴더구조와 관련된 부분들은 다루지 않았습니다. 디렉터리 구조 글에서 store는 빠졌는데 현재 디렉토리 구조 유지하면 될 거 같습니다.

  1. server의 db와 통신한다는 의미로 적어놨는데 server라고 적는 표현이 더 맞을수 있겠게요! 보통 서버에서의 repository는 db에 접근하는 레이어여서 저렇게 적긴 했습니다.
  2. repository가 model입니다.
  3. 전역상태(client데이터)는 react component인 view에서 다룹니다.
brightchul commented 1 year ago

3번 답변에 전역상태는 view에서 다룬다고 하셨는데, 클라이언트 전역 상태는 MVVM 에서 Model에 해당 합니다.

[전역 상태와 redux 관련 이야기]
zustand가 redux의 아이디어를 차용했으니 잠깐 redux 이야기를 해보겠습니다. redux 자체는 순수함수만 받기 때문에 API 처리를 위해서 thunk 등의 미들웨어를 받습니다. thunk를 보면 미들웨어로 중간에 서버 통신을 하지만 그것에 대한 결과를 redux에 넘겨줘서 redux를 통해 관리되는 전역상태에 반영이 됩니다.

다만 이렇게 하면 간단한 API 호출에 대해서도 전부다 보일러 플레이트 코드들을 만들어줘야 하고, 반복적인 서버 상태 (isLoading 등) 도 동시에 관리해줘야 했습니다.

그래서 순수 클라이언트 상태가 아니라 서버 상태까지 섞이는 일이 발생했고, 이러한 문제를 해결하기 위해 react-query가 나왔습니다.

react-query가 이러한 배경에서 나왔던 라이브러리라서 여러기능 + 캐시 기능 까지 있다보니 viewModel 쪽이어도 괜찮긴 합니다.

반면 전역 상태 스토어는 이러한 기능은 없고 순수 상태와 그 상태에 대한 변경에 대한 기능입니다. 그래서 이건 모델이라고 봐야 할 거 같은데 그러면 repository와의 관계 는 어떻게 하면 좋을까요?

현재 코드를 봤을 때

  1. repository의 경우 개별 URL로 호출하는 기능들이 주로 들어가 있으며
  2. 뷰모델은 이 Repository에 infrastructure 가 주입된 결과물을 react-query를 활용해서 사용하고 있습니다.

~제가 제시하고픈 의견은 다음과 같습니다.~

Updated : 클라이언트의 전역 상태 스토어는 Domain 계층으로 보는게 더 적절한거 같습니다.

brightchul commented 1 year ago

6월 25일 논의 정리