기술적으로 어려운 문제를 해결한 경험: Redux 도입기

동동·2021년 10월 4일
7

1. BE 5명, FE 2명으로 구성된 팀 프로젝트를 2021년 6월 말부터 현재까지 진행하고 있으며, FE 전반을 담당하고 있습니다

2. 프로젝트 진행 중 서버로부터 받는 API Response 의 일부가 변경되었으나, 이로 인해 파일이 11개나 변경된 적이 있습니다. 코드 구조가 변경에 매우 취약하여 유지보수성이 낮다는 문제에 직면하였습니다

type Job = {
  name: string;
  status: "WAITING" | "COMPLETED" | "CANCELED" | "RUNNING";
};

type RunningJob = {
  name: string;
  status: "RUNNING";
};

// 기존
type GpuServer = {
  // ...중략,
  jobs: Job[];
};

const Component = () => {
  const gpuServer = useAsyncHook<GpuServer>();

  const runningJobName = gpuServer.jobs.find((job) => job.status === "RUNNING")?.name ?? "N/A";
  const waitingJobCount = gpuServer.jobs.filter((job) => job.status === "WAITING").length;
  }, 0);

  return (
    <div>
      <span>현재 실행중인 Job: {runningJobName} </span>
      <span>대기중인 Job의 개수: {waitingJobCount}</span>
    </div>
  );
};

// 변경후
type GpuServer = {
  // ...중략,
  runningJob: RunningJob | null;
  waitingJobCount: number;
};

const Component = () => {
  const gpuServer = useAsyncHook<GpuServer>();

  return (
    <div>
      <span>현재 실행중인 Job: {gpuServer.runningJob?.name ?? "N/A"} </span>
      <span>대기중인 Job의 개수: {waitingJobCount}</span>
    </div>
  );
};
  • 기존에는 프론트에서 현재 실행중인 Job(runningJob)과 대기중인 Job의 개수(waitingJobCount)를 계산하였으나, 서버측에서 runningJobwaitingJobCount를 계산하여 프론트로 보내주는 방식으로 변경되었습니다.
  • 따라서 API Response의 변경은 runningJobwaitingJobCount 계산하는 부분을 삭제하면 되니 간단한 대응이라고 생각하였으나, 실제로 변경에 대응하다보니 11개의 파일이 변경되었습니다.

3. 왜 코드가 변경에 취약한 것인지 파악하기 위해 현재 코드의 구조도를 작성하였습니다

  • 참고) 로그인 페이지와 Gpu서버 상세 조회 페이지의 구조도 ![로그인 페이지와 Gpu서버 상세 조회 페이지의 구조도]

구조도를 작성하며 깨달은 현재 코드의 문제점은 다음과 같습니다.

  1. 컴포넌트가 너무 많은 역할을 하고 있습니다.
    • API를 호출하여 서버와 통신하는 로직과 runningJobwaitingJobCount 계산하는 비즈니스 로직 모두 가지고 있습니다.
    • 따라서 runningJobwaitingJobCount 를 필요로 하는 모든 컴포넌트에서 각각 runningJobwaitingJobCount를 계산하고 있었기에 이를 모두 수정하여야 합니다.
  2. 컴포넌트가 커스텀 훅을 통해 서버에 바로 통신하고 있습니다. 서버의 Response를 컴포넌트에 곧바로 전달하여, Response 변경의 영향을 컴포넌트가 직접적으로 받습니다.
  3. 타입 설계가 세밀하지 못합니다.
    • API Response 의 타입을 컴포넌트에서도 그대로 사용하고 있습니다.
    • 따라서 API Response 가 바뀌면, 이 타입을 사용하는 모든 컴포넌트가 다 수정되어야 합니다.

⇒ 역할과 책임을 구분하는 레이어가 명확하지 않아, 변경에 취약한 구조라고 결론내렸습니다.

4. 구조를 변경하겠다는 목표를 세우고, 개선방안을 도출하였습니다

개선방안 1. 서비스 계층을 분리하고 커스텀 훅에서 비즈니스 로직을 담당합니다.

  • 서비스 계층에서 HTTP 통신과 관련된 에러 핸들링을 수행합니다.
  • 커스텀 훅에서 서버의 Response를 Domain 데이터로 바꾸는 비즈니스 로직을 담당합니다.

개선방안 2. 상태관리 툴을 도입합니다.

  • 상태관리 툴 없이 React 내장 함수만을 사용했을 때 서버와의 통신 모습은 다음과 같습니다.

  • hook을 통한 로직의 재사용은 가능하지만, 데이터 측면에서는 각 컴포넌트가 각각의 데이터를 가지고 있어 도메인과 관련된 상태가 파편화되어 있습니다.
  • 상태 변경과 상태 사용이 항상 하나의 컴포넌트 안에서 이루어지기 때문에, 도메인과 관련된 상태를 서버로부터 가져오기 위해 필요한 모든 로직을 각 컴포넌트에서 수행해야합니다.
  • 상태관리 툴로 리덕스를 사용한다면 서버와의 통신 모습은 다음과 같이 바뀔 것입니다.

  • 상태 변경과 상태 사용이 분리됩니다. 상태는 스토어에 저장되므로, 컴포넌트에 속하지 않습니다. 상태 변경을 요청하는 컴포넌트와 상태를 사용하는 컴포넌트가 일치하지 않아도 됩니다. 상태가 어떻게 변경되는지는 상태 변경을 요청하는 컴포넌트와 상태를 사용하는 컴포넌트의 관심사가 아닙니다. 상태 변경은 리덕스의 관심사입니다. 즉, 상태 변경 로직과, 상태를 사용하여 UI 로직을 분리할 수 있습니다.
  • 리액트는 상태 변경에 따른 UI 변경에 집중합니다. 도메인 관련 상태를 어떻게 변경시킬지를 정하는 비즈니스 로직, 서버와의 통신은 모두 리덕스에서 관리합니다.
  • 리덕스를 도입한다면 컴포넌트와 상태의 구조는 다음과 같이 변경되어 각 부분의 역할과 책임이 명확해집니다.

  • Component: Selector를 통해 가져온 상태를 렌더링하는 역할과 action 을 dispatch 하는 역할을 수행합니다. Selector가 Store의 상태 구조를 추상화하므로, Component는 Store의 상태 구조가 변경되더라도 영향을 받지 않습니다.
  • Action Creator: 서비스 계층을 통하여 서버와 소통합니다.
  • Service: 401과 같은 공통적인 에러, HTTP 통신과 관련된 에러 핸들링을 수행합니다. API Endpoint에 대한 정보도 Service만 가지고 있습니다.
  • Reducer: API Response 를 Domain 상태로 변경하는 비즈니스 로직을 담당합니다.

⇒ 기존에 waitingJobCount, runningJob 을 Reducer에서 계산했다면, 사용하는 Component는 API Response가 변경되더라도 Store에서 상태를 가져오는 것은 동일하므로 아무런 변경 사항이 없었을 것입니다. 즉, 레이어를 구분한다면, 역할과 책임을 명확히 하여 변경사항이 전파되는 경계점을 쉽게 파악할 수 있을 것으로 예상됩니다.

5. 리덕스 도입을 결정하였습니다

  1. 현재 프로젝트에는 gpServer, job, member 의 3가지 도메인이 존재합니다.

  2. 최소 필수 기능은 모두 구현된 상태에서 리덕스를 전면적으로 도입하는 것은 많은 코드 수정을 불러일으키므로, 여파를 최소화 하기 위해 가장 로직이 간단한 member 부터 도입하기로 결정하였습니다.

  3. memberSlice 의 구조도는 아래와 같습니다.

6. 현재 설계를 마무리하였으며 member 기능을 리덕스로 마이그레이션 중입니다

기존 Login Page 구조

리덕스 도입 후 Login Page 구조

profile
작은 실패, 빠른 피드백, 다시 시도

0개의 댓글