23년 3월 29일 TIL & 개발노트 (코드 리팩토링, 배포환경)

Posted by , March 30, 2023
TIL
Series ofTIL

thumbnail

흑우집합소 개발을 하며...

PS : 아래 예제에서 호출되는 url은 참고용입니다


개발이 거의 끝나가는 무렵이었다.
사실 막 짠 코드가 좀 있었고...

로직이 좀 엉상한 부분도 있었다.
그래서 미래에 또 헤매거나 복잡함을 피하기 위해 찌금 작업을 좀 해두자 라는 마인드로...
일부 리팩토링을 진행했다.

1. 똥이 되어가는 코드를 코드 리팩토링

프론트엔드 영역에서 Axios를 호출하는 부분에 대한 고민이 깊어졌다.

await excuteAxiosInbound({
  inboundRequestConfig: {
    url: "/api/test/some/url",
    data: { url: "acc/logout", method: "post" },
  },
})
  .finally(() => {
    setLoading(false)
  })
  .then(responseData => {
    //로그이웃 처리 및 기타 작업 ing~
    router.push("/gogo/somewhere")
  })
  .catch(e => {
    axiosClientErrorHandler({ axiosError: e })
  })

이렇게 되어 있다.
물론 전체 코드는 아니고 호출되는 영역만 가져왔다.

근데 이걸 매번 호출할 때마다 반복적으로 작성해야 하는 부분이 마음에 안들었다.
지금 호출 로직은 아래의 세 가지 문제점이 발생한다.

  1. 저 호출 부분에서 성공 시 또는 실패 시 처리해야 할 공통 로직이 추가된다면 일일히 바꿔야 하는 문제가 생긴다.
  2. setLoading과 같은 상태 관리(Zustant)의 로직을 호출 컴포넌트마다 작성해야 한다.
  3. 매번 인바운드(내부 api 주소로 호출하는 영역) 부분의 주소를 적거나 아웃바운드(백엔드 쪽)로 내보내야 하는 부분이 겹칠 때가 있다.

이런 문제로 인해 저 부분을 공통 로직으로 만들어 쓰기로 하고...
이를 해결하기 위해 몇 가지 고민을 했다.

  1. then 성공 로직에서 성공 시 공통으로 처리할 함수를 적어주기 (예외 발생시 처럼)
  2. excuteAxiosInbound 함수 자체를 전달인자로 바꾸기
  3. inbound로 나가는 axios를 개선

해결법에서 1번은 사실 좋은 선택은 아니었다.
만들어갈 결과물에 포함될 방법일 뿐 그 자체만으로는 해결이 되지 않는다.

2번은 생각해보니 성공 실패 로직에 대한 것을 호출 영역에서 함수로 전달하면 될 것 같다는 생각이 들었다.
3번은 지금도 되어 있긴 한데 문제 2번의 스토어를 넣어서 같이 사용하는 방향으로 하면 좋을 것 같았다.

그래서 해결을 위해 호출하는 공통 로직을 Custom Hook으로 만들어야 했다.

import { useCallback } from "react"
import { SweetAlertOptions } from "sweetalert2"
import { SendAxiosRequestConfig } from "../lib/axios/axios.type"
import { excuteAxiosInbound } from "../lib/axios/axios.util"
import { axiosClientErrorHandler } from "../lib/axios/errors/axios.client.error.handler"
import useSetAccountInfo from "./stores/use.account.info"
import useLoading from "./stores/use.loading"

export interface AxiosCallClientHandlerInputDto {
  outboundData: SendAxiosRequestConfig
  successAction: Function
  failSwal: SweetAlertOptions
  setLoadingAction?: Function
  outboundHeader?: { [key: string]: string } //Outbound로 헤더 보낼 때 쓰는 속성
  inboundUrl?: string //inbound 호출 URL이 다른 경우 별도 작성 (Default = api/hub)
}

/**
 * Axis 내부 호출 Hook
 * @returns
 */
export function useAxiosInbound() {
  const { handleSetLoading } = useLoading()
  const { handleLogoutAccountInfo } = useSetAccountInfo()

  //변수 선언
  const axiosCallClientHandler = async (
    params: AxiosCallClientHandlerInputDto
  ) => {
    //로딩
    handleSetLoading(true)

    await excuteAxiosInbound({
      inboundRequestConfig: {
        url: params?.inboundUrl
          ? params.inboundUrl
          : "/use_api/back/inbound/axios",
        data: params.outboundData,
      },
      outboundHeader: params.outboundHeader,
    })
      .finally(() => {
        handleSetLoading(false)
      })
      .then(responseData => {
        params.successAction(responseData)
      })
      .catch(e => {
        axiosClientErrorHandler({
          axiosError: e,
          handleLogoutAccountInfo,
          failSwal: params.failSwal,
        })
      })
  }

  //메모제이션을 해준다.
  const handleExcuteAxiosInbound = useCallback(
    (params: AxiosCallClientHandlerInputDto) => {
      axiosCallClientHandler(params)
    },
    [axiosCallClientHandler]
  )

  return { handleExcuteAxiosInbound }
}

이 Hook은 handleExcuteAxiosInbound라는 함수를 반환하는데 여기에 호출할 inputDto인 AxiosCallClientHandlerInputDto를 전달하면 된다.
그래서 호출할 컴포넌트에서는 아래와 같이 간단하게 호출할 수 있었다.

그리고 useCallback을 이용해 재사용성을 하게 했다.

interface PagingProps {
  //....
}

export default function PagingComponent(props: PagingProps) {
  //some logic..

  const { handleExcuteAxiosInbound } = useAxiosInbound()

  const handle4PageChange = async (page: number) => {
    handleExcuteAxiosInbound({
      outboundData: {
        url: "some/page/data/list?page=" + page + "&pnum=10",
        method: "get",
      },
      successAction: (responseData: any) => {
        clearDataList()
        const listData = responseData.data.result.resultData
        setDataList(listData.dataList)

        //페이지 데이터
        const pageData = listData.pageData

        setNowPage(pageData.nowPage)
        setTotalItemCnt(pageData.totalItemCnt)
        setTotalPage(pageData.totalPage)
        setPerPageNum(pageData.perPageNum)
      },
      failSwal: {
        title: "데이터 로드 실패",
        html: "페이지 데이터 로드에 실패하였습니다. <br> 관리자에게 문의해 주세요.",
        icon: "error",
        confirmButtonText: "확인",
      },
    })
  }
  return <></>
}

개선된 코드는 간단 명료하다.
axios를 사용할 곳에 **useAxiosInbound()**라는 hook을 호출하여 실제 Axios 작업을 하는 함수를 불러온다.

성공은 호출되는 영역마다 작업이 상이해서 저렇게 함수를 전달하지만,
실패는 언제나 지정된 양식이 있기에 공통으로 처리하고, 그 이외 예외는 알럿을 호출할 수 있게 해줬다.

저 inputDto 자체는 재사용이 안되고, 호출 지점마다 고유해서 이 정도만 해도 많이 개선이 되었다고 판단했다.

2. 배포만 하면 문제...

이제는 몇 가지 테스트를 위해 배포를 해봤는데 자꾸 몇 가지 기능이 정상적으로 동작하지 않았다.
로그인 부분과 이중 로그인 영역 쪽이었다.

그래서 20분 고민하다가...
내 로컬과 배포 환경과의 차이점이 뭘까?

  1. pm2 구동환경
  2. 방화벽
  3. nginx
  4. aws와 Cloudflare의 환경

그래서 각 부분을 하나씩 점검했다.
1번의 경우 문제는 아니었다.
어짜피 구동하는건 로컬이나 pm2로 올리나 똑같으니까?

2번 방화벽도 재점검 했는데 문제는 아니었다.
백엔드는 지정된 ip와 서버 내, 그리고 nginx에서 제대로 프록싱 하고 있었다.

3번...이 정답이었다.
Nginx는 기본적으로 헤더를 알려진 값 외에는 막는다.

그래서 내가 커스텀으로 사용한 헤더가 정상적으로 전달이 되지 않은 문제가 답이었다.
이와 관련된 내용은 Nginx에서 헤더 전달하기 (프록시 사용) 포스팅을 참고하자.


정리

역시 코드를 잘 작성하는 부분이 중요하다.
백엔드의 경우 Layer 아키텍쳐에서 포트 앱 어댑으로 바꾸고 나서 정말 코드 보기가 편해졌다.

그리고 리팩토링...
개인적으로 리팩토링은 코드를 계속 지속 가능성이 될 수 있도록 해주는 중요한 수단인 것 같다.

배포환경...
빨리 찾아서 다행이지...
좀만 더 늦었으면 와인 까서 마시면서 엄한 곳이나 쑤셨을 듯 싶다....