I code, therefore I exist.

웹 프론트 엔드 개발을 공부하고 있는 Ocean이라고 합니다. 만나서 반갑습니다.

WEB/REACT

React의 의존성 주입

Ocean 2025. 3. 27. 22:27

들어가며..

안녕하세요, Ocean입니다.

 

입사한지 3개월 차인 우당탕탕 신입 개발자입니다. 이번에 팀장님에게 코드 리뷰를 받던 중 흥미로운 부분이 있어서 개발자 단톡방에서 질문도 하고 이야기도 나누고 했던 부분을 정리하고자 합니다. 


1. 문제의 코드

어떠한 상태를 변경하는 `StatusChangeButton` 컴포넌트입니다. 

Status를 업데이트한 후의 로직은 컴포넌트를 사용하는 모든 곳에서 같지만, 해당 Status가 어떤 주체의 상태인지에 따라서 api가 달라지기 때문에 mutate를 외부에서 받습니다.

(*ex: 사람이라면 personMutate, 자동차라면 carMutate)

또한 `mutate` 이후에 로직인 `refetch`와 쿼리 키 무효화를 위한 쿼리 키도 전달 받습니다.

// 외부에서 mutate를 전달
const StatusChangeButton = (mutate, queryKey, refetch) => {
  const [status, setStatus] = useState();
  const [changeReason, setChangeReason] = useState();

  // ... 내부에서 status와 ChangeReason을 수정

  const handleStatusChange = () => {
    mutate(
      { params: { status, changeReason } },
      {
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: queryKey });
          showToast('상태변경 완료', 'success');
          refetch();
        },
        onError: () => {
          showToast('상태변경 실패', 'error');
        },
      }
    );
  };

  return <button onClick={handleStatusChange}></button>;
};

 

StatusChangeButton의 내부에서 관리하는 state(status, changeReason)를 mutate로 잘 전달할 수 있기 때문에 문제가 없어 보이기도 합니다.

 

하지만 "Status를 업데이트한 후의 로직"이 추 후에 달라지면 어떻게 될까요?

처음에 설계할 때는 사용하는 모든 곳에서의 onSucess, onError 로직이 같아서 해당 로직이 컴포넌트 내부에 있었지만, 만약 다른 로직을 수행하게 하고싶다면 해당 로직 내부에 분기 처리를 해야 할 겁니다....

 

// bad practice... 😭
const StatusChangeButton = (mutate, queryKey, refetch, type) => {
  const [status, setStatus] = useState();
  const [changeReason, setChangeReason] = useState();

  // ... 내부에서 status와 ChangeReason을 수정

  const handleStatusChange = () => {
      mutate(
        { params: { status, changeReason } },
        {
          onSuccess: () => {
            if(type === 'person') {
              queryClient.invalidateQueries({ queryKey: queryKey });
              showToast('상태변경 완료', 'success');
              refetch();
            } else {
             // ... 다른 로직
            }
          },
          onError: () => {
            showToast('상태변경 실패', 'error');
          },
        }
      );
    };

  return <button onClick={handleStatusChange}></button>;
};

 

이렇게 하드하게 분기되는 경우가 많아지면 많아질수록, 컴포넌트는 점점 더 비대하고 복잡해질 겁니다.. 따라서 유지보수가 힘들어지겠죠..

그리고 mutate가 아닌 일반적인 fetch 메서드는 사용할 수 없습니다. 해당 로직은 mutate에 너무 강하게 결합되어 있습니다..


2. 수정된 코드

// Good Refactoring!
const StatusChangeButton = ({handleStatusChange}:{handleStatusChange: (status:string, changeReason: string) => void;}) => {
    const [status, setStatus] = useState();
    const [changeReason, setChangeReason] = useState();
  
    // ... 내부에서 status와 ChangeReason을 수정
  
    return <button onClick={()=>handleStatusChange(status, changeReason)}></button>;
  };

 

내부에서 작성되었던 handleStatusChange를 외부에서 주입 받습니다. 내부의 state인 status와 changeReason을 전달해야 하기 때문에, 외부에서 주입 받는 함수의 타입을 (status:string, changeReason: string) => void;로 정의합니다.

 

const handleStatusChange = (status: string, changeReason: string) => {
  mutate(
    { params: { status, changeReason } },
    {
      onSuccess: () => {
        if (type === 'person') {
          queryClient.invalidateQueries({ queryKey: queryKey });
          showToast('상태변경 완료', 'success');
          refetch();
        } else {
          // ... 다른 로직
        }
      },
      onError: () => {
        showToast('상태변경 실패', 'error');
      },
    }
  );
};

// 사용하는 쪽, 즉 상위에서 handleStatusChange를 전달
<StatusChangeButton handleStatusChange={handleStatusChange} />;

 

상위에서 status와 changeReason을 매개변수로 받는 함수를 만들어서 StatusChangeButton으로 전달해줍니다....

 

이제 StatusChangeButton로직에서 자유로워졌습니다!! 👏 👏 👏 🦅

StatusChangeButton은 handleStatusChange의 로직에 대해선 관심을 끌 수 있습니다.

그저 자신의 state를 전달해서 실행해주면 됩니다!!

 

function fetchStatusChange(status, changeReason) {
  return fetch('/api/status-change', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ status, changeReason }),
  })
    .then((res) => res.json())
    .then(() => {
      if (type === 'person') {
        queryClient.invalidateQueries({ queryKey: queryKey });
        showToast('상태 변경 완료', 'success');
        refetch();
      } else {
        // ... 다른 로직
      }
    })
    .catch(() => {
      showToast('상태 변경 실패', 'error');
    });
}

<StatusChangeButton handleStatusChange = {fetchStatusChange} />

 

또한 이제 mutate가 아닌 일반 fetch 메서드도 전달할 수 있습니다!!

이로써 StatusChangeButton은 강결합을 줄이고, 보다 유연한 컴포넌트로 변화했습니다!!

 


3. 의존성 주입

위와 같이 여러 컴포넌트에서 공통된 기능을 사용할 때, 특정한 기능을 각 컴포넌트 내부에서 직접 생성하는 것이 아니라, 외부에서 주입받아 사용하는 방식을 "의존성 주입 (Dependecy Injection)"이라고 합니다. 

의존성 주입을 적용하면, 컴포넌트가 특정 로직에 직접 의존하지 않기 때문에 유지보수가 쉬워지고, 재사용성이 높아집니다!

 


마무리하며..

코드 리뷰를 처음 받을 때는 무슨 말인지 이해하기 어려워 난감했던 경험이 많았습니다. 하지만 계속 보고 듣다 보니 점점 이해할 수 있게 되더군요.

특히, 복잡한 컴포넌트를 수정해야 할 때 처음에는 막막했지만, 시간이 지나며 로직이 보이기 시작했습니다. 의존성 주입 같은 개념도 처음에는 낯설었지만, 차근차근 배우다 보니 자연스럽게 이해할 수 있었습니다. (물론 어려운 개념은 아니긴 합니다...ㅎㅎ)

어떤 개념이든 시간을 들여 꾸준히 학습하면 익숙해질 수 있다는 걸 깨닫게 되었습니다. 앞으로 SOLID 원칙이나 단일 책임 원칙도 정리해 보면 좋을 것 같습니다.

개발자 여러분, 특히 신입 개발자 여러분! 모두 화이팅입니다! 🚀

'WEB > REACT' 카테고리의 다른 글

forwardRef와 useImperativeHandle  (1) 2024.12.22
useState와 클로저  (2) 2024.12.12
useReducer  (1) 2024.06.29
useContext  (0) 2024.06.19
useMemo  (0) 2024.06.19