들어가며..

아이메디신에는 isyncbrain 3.0이라는 뇌파데이터를 활용하여 분석 및 결과지를 확인할 수 있는 웹사이트가 있습니다.

이 사이트에서는 react를 활용하여 ui를 구성하고, 상태관리 라이브러리는 redux와 redux-saga를 이용하여 상태관리 및 비동기 서버통신을 진행하였습니다.

redux-saga란?

일단 먼저 redux-saga가 무엇인지부터 이야기를 해야 할 것 같습니다.

redux-saga는 generators라는 es6 기능을 이용하여,비동기 흐름을 제어할 수 있는 라이브러리입니다. saga에는 task들을 동시에 진행할 수도 있고, 취소할 수도 있는 다양한 기능을 제공하고 있습니다. 그리고 redux의 미들웨어이기 때문에 redux에 접근하여 작업을 dispatch 할수 있습니다.

isyncbrain에는 redux에 관리하는 정보들이 매우 많기 때문에 이런 라이브러리가 필요했고, saga를 채택하여서 구축하여 서비스 오픈까지 진행하였습니다.

프로젝트에서 saga를 사용한 방식을 큰 구조로 보자면 먼저 아래와 같이 slice파일에 정의 리듀서 코드를 작성한 뒤

export const patientSearchSlice = createSlice({
  name: 'patientSearchSlice',
  initialState,
  reducers: {
    searchPatientRequest(
      state: PatientSearchState,
      action: PayloadAction<PatientSearchRequest>,
    ) {
      state.loading = true
      state.error = null
    },
    searchPatientSuccess(
      state: PatientSearchState,
      action: PayloadAction<PageResponse<Patient>>,
    ) {
      state.loading = false
      state.error = null
      state.patients = patients
      state.totalPages = totalPages
      state.totalElements = totalElements
      state.pageIndex = pageNumber
      state.itemPerPage = size
    },
    searchPatientError(state: PatientSearchState, action: PayloadAction<any>) {
      state.loading = false
      state.error = action.payload
    },
  },

  export const {searchPatientRequest, searchPatientSuccess, searchPatientError} =
  patientSearchSlice.actions
})

saga파일에 generator문법을 활용하여 해당 작업을 정의한 함수를 선언합니다.

function* fetchPatient() {
  try {
    const orgResponse: ModelResponse<Organization> = yield call(fetchOrgApi)
    yield put(fetchOrgSuccess(orgResponse.data))
  } catch (e) {
    yield put(fetchOrgFailure(e))
  }
}

그리고 필요한 view 쪽에서 saga 함수를 dispatch하여 사용하였습니다.

  const handleSearch = (search: AnalysisSearchKind) => {
    dispatch(searchIndividualEeg.request({...query, search}))
  }

과연 우리 프로젝트에 적합한 라이브러리인가?

이러한 형태는 서비스를 출시할 당시에는 큰 문제는 생기지 않았습니다. 하지만 서비스에 추가 기능들이 들어가게 되고, 주니어 개발자(바로 접니다)가 isyncbrain 프로젝트에 합류하게 되면서부터 프론트 담당자들의 고민은 시작되었습니다.

saga가 다양한 기능을 제공하고 있는 것은 좋지만, 회사 특성상 없던 기능이 생기는 경우도 잦았는데 이럴 경우에 API 호출하는 곳의 코드를 바꾸려고 하면 최소 파일 3개를 수정하고 간단한 API호출의 경우에도 동일하게 많은 양의 코드를 작성해야 했습니다. 또한, 기능 추가가 될 때마다 위에 작성한 함수들을 정의하고 view쪽으로 리턴값을 받아오기 힘든 구조여서, 불필요하게 중복적으로 작성되는 코드들이 늘어갔습니다.

프론트 담당자들끼리 많은 논의를 한 끝에 추후의 유지 보수와 새기능 추가시 코드 생산성을 위해서 redux-saga를 변경하기로 하였습니다. recoil이나 아예 상태관리 라이브러리를 교체하자 이런 의견도 있었고, redux-saga는 유지하되 서버 통신 부분만 react-query를 이용하자 등등 많은 의견을 주고 받은 끝에 redux-thunk로 마이그레이션을 하기로 결정하였습니다. 그이유는

첫번째로, RTK를 활용하면 불필요한 보일러 플레이트 코드 및 중복적으로 작성되는 코드의 양을 줄여서 코드 생산성을 향상하고, 원하는 수준의 비동기 서버 통신 로직 구현이 가능합니다.

프로젝트 진행 시 필요한 비동기 로직이 그리 복잡한 편이 아니였기 때문에 RTK의 createAsyncThunk 활용하여 구현하여도 충분했습니다.

두번째로, Confirm Dialog를 재사용성을 고려하여 사용하려면 view쪽에 리턴값을 수월하게 받기 위해 비동기 작업이 완료된 후 view쪽에 작업완료가 되었음을 알려주는 callback이 필요했습니다.

하지만 saga에서는 그렇게 할수 없기에 saga함수를 정의하는 쪽에 Confirm Dialog 로직을 넣는 방식으로 작업하였는데, 많은 양의 Confirm Dialog의 로직을 하나하나 다 넣기에 공수가 많이 드는 작업분량이었습니다. 따라서 Confirm Dialog에서 사용자의 선택값만 리턴으로 받아올수 있다면, 컴포넌트화 혹은 훅으로 만들어서 view단에서 간단하게 처리할수 있는 작업을 할수있을 거라고 생각했습니다.

redux-thunk로의 마이그레이션

먼저 Slice파일에 createAsyncThunk를 이용하여 action함수를 선언하고 동일하게 리듀서 코드를 작성하였습니다.

export const fetchOrgReadAction = createAsyncThunk(
  "api/org/get",
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetchOrgApi();
      return response.data;
    } catch (error) {
      return rejectWithValue(error);
    }
  }
);

export const orgSlice = createSlice({
  name: "user",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchOrgReadAction.pending, (state) => {
      state.loading = true;
      state.error = undefined;
    });
    builder.addCase(fetchOrgReadAction.fulfilled, (state, action) => {
      state.loading = false;
      state.organization = action.payload;
    });
    builder.addCase(fetchOrgReadAction.rejected, (state, action) => {
      state.loading = false;
      state.error = action.payload;
    });
  },
});

그리고 원하는 곳에서 dispatch하여 사용할수 있습니다. 아래와 같이 훅으로 만들어 리턴값을 받아와서 view단에서 간단하게 다음 작업을 할 수 있게 하였습니다.

  const onFetch = () => dispatch(fetchOrgReadAction())
    ...
  onFetch(oid).then(() => {
      onChangeClicked(true)
    })

이 글을 마치며

redux-thunk가 우리의 구미에 맞는 완벽한 대안은 아니어도 보다 더 유연하게 활용할 수 있게 된 것이 가장 큰 성과였습니다. 프로젝트의 특성에 맞게 어떠한 것이 최선일지 많은 의견을 이야기하며 맞추어 갈 수 있었던 것이 가장 좋은 경험이였습니다.

앞으로도 이 프로젝트를 유지 보수하면서 최선의 대안이 뭔지 또한 코드를 작성하며 생기는 문제점들을 파악하고 서로 커뮤니케이션 하며 그것의 개선 방법을 찾아서 진행해야 할것입니다.