xction-dev / xction.co.kr

Xction!의 홈페이지를 만들고 있습니다
0 stars 0 forks source link

Architecture - MVI & Clean Architecture #39

Open nuagenic opened 7 months ago

nuagenic commented 7 months ago

공부하면서 느낀 거 적으려다 보니 PR 리뷰로는 좀 길어질 거 같아서 따로 이슈로 작성해 보겠습니다. 제가 맞게 이해하고 있는지, 혹시 잘못된 부분 있으면 코멘트 남겨주시면 감사하겠습니다.

우리의 목표

Clean Architecture와 MVI 패턴을 결합시키는 것. 현재 우리의 레포지토리는 Clean Architecture만 따르고 있습니다.

Clean Architecture 리뷰

Clean Architecture 자체는 Domain Layer-Application Layer-Adapter Layer로 이루어져 있으며, 우리는 이를 Entity-Usecase-(백엔드 서버와 통신하는) Usecase의 구현체(React Hook)로 대응시켰습니다.

사실 아직 불명확한 것은, Usecase의 구현체를 사용하는 UI 단위의 modules도 Adapter Layer에 포함되는가? 입니다만, Clean Architecture 안에서는 로직만 다룬다고 생각해 별개의 영역으로 생각했습니다.

User의 경우

  1. User Entity가 있다.
    • 유저에 대한 추상화된 객체. 들어가야 하는 속성과 type만 명시해줄 뿐
  2. User를 다루는 usecase가 있다.
    • 여기서 중요한 것은 User가 주어가 아니라 목적어에 해당한다는 점이 아닐까 싶습니다. User가 Video를 보는 시나리오는 Video의 usecase로 분류되지만, User가 User의 정보를 수정하는 시나리오는 User의 usecase일 테니까요. 사실 웹 내에서 이루어지는 거의 모든 시나리오의 주어가 User이기도 하고요.
    • 그렇다면 User를 다루는 usecase에서 주어와 목적어는 고정된 셈입니다. 따라서 각각의 usecase들을 나누는 것은 동사일 것입니다. 따라서 모든 usecase의 시작을 동사로 하는 것이 어떨지 제안합니다. 현재는 UserAccessServiceCommentService 등이 존재하는데, 이들을 LoginServiceGetCommentService로 수정하는 것이 더 직관적이고 바람직해 보입니다.
    • 그렇게 된다면 User Entity의 경우 가능한 usecase는 다음과 같습니다.
      • SigninService : 회원가입을 위한 유즈케이스
      • LoginService : 로그인을 위한 유즈케이스
      • SignoutService : 회원 탈퇴를 위한 유즈케이스
      • LogoutService : 로그아웃을 위한 유즈케이스
      • ReadMeService : 내 유저 정보를 조회하는 유즈케이스 (마이페이지 등)
      • UpdateMeService: 내 유저 정보를 수정하는 유즈케이스
      • ReadFollowerService : 내 팔로워를 조회하는 유즈케이스
      • ReadFollowingService : 내 팔로잉을 조회하는 유즈케이스
    • 사실 이렇게까지 세분화가 되야 할지는 잘 모르겠습니다... 지금은 거의 API 엔드포인트 하나에 하나의 유즈케이스를 대응시킨 셈인데, 보통 이렇게 대응시키는 게 맞을까요? 가령 지금 구현되어 있는 userAccessService의 경우, 로그인과 로그아웃이 하나의 유즈케이스로 구현되어 있습니다. 유즈케이스를 구분하는 기준이 정확히 무엇일지 자문이 필요합니다..
  3. 상황에 맞는 DTO를 주입시켜 유즈케이스의 시나리오를 구현하는 리액트 훅이 있다.

MVI와 어떻게 연결되는가?

MVI를 리액트 내에서 어떻게 구현해야 할까... 여기 Readme를 참고해 보았는데, 간단하게 정리하자면 이렇습니다.

즉, view 부분에서 이밴트핸들러 등으로 호출하는 것은 intent가 아니라 intent를 파라미터로 가지는 update라는 함수입니다. 요런 식으로요.

let handler = (event) => {
        model = update(model, model.running ? 'STOP' : 'START');
    };

이 구조를 참조해서, 저희 Clean Architecture에 적용을 한다면, 유즈케이스의 구현체인 리액트 훅은 model(현재 우리 아키텍쳐 안에서는 DTO라고 생각됨)을 반환하는 함수 형태여야 할 것이고, 이 함수에 파라미터로 들어가는 intent는 다른 방식으로 정리가 되어야 하지않을까 하는데... 우선 여기까지만 생각해보았고 그렇다면 어떻게 정리할 것인지는 더 고민을 해봐야 할 것 같습니다.

lenyakim commented 7 months ago

1) 유즈케이스 참고 링크 : https://velog.io/@bky373/Architecture-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%9D%98-%ED%95%B5%EC%8B%AC-Use-Case

동영상 : https://youtu.be/zid-MVo7M-E?si=2zn_XdGzXujUzlpU

유즈케이스 관련 영상을 시청하고 든 생각입니다.

위쪽에서 정리해주신대로 유즈케이스가 기능별로 세분화되어 있는 게 좋은 것 같습니다. 그리고, 유즈케이스 네이밍을 동사로 시작하는 것도 좋은 명명법이라고 생각됩니다..!

다만, 유즈케이스를 주체가 되는 엔티티(Ex. Video 엔티티의 유즈케이스, User 엔티티의 유즈케이스)에 따라 분류하지 말고, 간단히 그 유즈케이스가 실행할 ’기능‘에 맞춰서 분류하고, 그 유즈케이스가 기능을 실행하기 위해 참고해야 하는 엔티티는 무엇인지 파악하는 편이 더 편리할 것 같습니다.

예시로,

ReadFollowerService : 내 팔로워를 조회하는 서비스

의 경우에,

유저 A가 자신의 팔로워를 조회할 수도 있고, 다른 유저 B의 팔로워를 조회해 볼 수도 있기 때문에, ’내 팔로워를 조회하는 서비스‘보다 ’유저의 팔로워를 조회하는 서비스‘로 정의하는 편이 편리할 것 같습니다.

특히 로그인/로그아웃 상태나 유저 정보에 영향을 받지 않는 기능들은 이렇게 broad하게 정리하는 게 편리할 듯 합니다.

반대로 UpdateMeService처럼 반드시 로그인 상태에서, ‘내’ 정보만 접속 가능해야 하는 유즈케이스들은 지금 정의를 유지해도 좋을 것 같습니다!

lenyakim commented 7 months ago

로그인 상태를 확인하는 요청을 하나의 기능을 가진 CheckLogin이라는 Usecase로 정의할 때 (실제로 이런 유즈케이스를 생성할 건 아니지만)

1)로그인이 필요한 요청 (Ex. UpdateMeService) UpdateMeService -> CheckLogin -> ~~

2)로그인이 필요없는 요청(Ex. View) ViewService -> ~~

이렇게 관리해도 괜찮을 것 같습니다. 그러면 서비스팀에서 저희가 개발 중에 특정 기능을 로그인 상태에서만 할 수 있도록 바꾼다거나, 아니면 전과 다른 엔티티들을 참고하도록 바꾼다거나 하는 식으로 기획이 달라져도 더 유연하게 대처할 수 있을 것 같아요.

. . .

MVI 부분과 관련해서, 아직 update 함수와 Intent 부분을 어떻게 관리해야 할지 조금 머리가 복잡한데…

우선 유저한테 직접 보이는 부분은 실제 엔티티가 아니라서, 적절히 updateContents 같은 usecase를 하나 만들어서 페이지 내용만 바꿔주면 될 것 같고… User나 Video처럼 엔티티를 직접적으로 바꾸는 유즈케이스들은 조금 더 찾아보겠습니다.

designDefined commented 7 months ago
export default SomeUI (){
  const { postData } = usePostView(postId)
  const { isLoggedIn } = useAuthView()

  return (
    <div>
      { postData.isMine && <EditButton /> }
      { isLoggedIn && !postData.isMine && <LikeButton /> }
      <CopyButton />
    </div>
  );
}
designDefined commented 7 months ago

이렇게도 할 수 있지 않을까?

Just<PostDto> = {
  data: PostDto
}

Async<Paginated<PostDto>>
Authed<PostDto> = {
  data: PostDto
  isAuthorized: true;
} | {
  data: null
  isAuthorized: false;
}

Maybe<PostDto>