4 minute read

익명 유저 대응 로직의 아키텍처화 고민 과정

서비스를 운영하다 보면 특정 기능은 로그인한 유저만 사용할 수 있도록 제한해야 하는 경우가 많습니다. 이때 단순히 기능을 막는 것에 그치지 않고, 자연스럽게 로그인을 유도하는 모달이나 바텀시트를 띄워주는 것이 사용자 경험(UX) 측면에서 매우 중요합니다.

하지만 프로젝트 규모가 커짐에 따라 이러한 익명 예외 처리가 여러 곳에 흩어지게 되면서 몇 가지 문제에 직면했습니다.

문제점: 반복되는 ‘익명 예외 처리’와 UX 파편화

  • 반복되는 보일러플레이트: 컴포넌트나 페이지를 만들 때마다 익명 체크 로직과 모달 트리거 코드를 매번 직접 작성해야 했습니다.
  • 의존성 증대: 모달을 띄우기 위해 하위 컴포넌트로 콜백 함수를 전달하는 Props Drilling이 발생하며 컴포넌트 간의 결합도가 불필요하게 높아졌습니다.
  • 휴먼 에러와 UX 파편화: 개발자가 체크 로직을 누락하면 어떤 기능은 모달이 뜨고, 어떤 기능은 아무 반응이 없는 등 UX의 불일치가 발생했습니다. 유저 입장에서는 “방금은 됐는데 이건 왜 안 돼?”라는 의문을 갖게 만들기도 합니다.

이러한 문제들을 해결하고, 더욱 선언적이며 효율적인 구조를 만들기 위해 세 단계에 걸쳐 아키텍처를 개선해 보았습니다.

1차 시도: 이벤트 기반의 전역 가드 (AnonymousLoginGuard)

가장 먼저 시도한 방식은 Pub/Sub 패턴을 활용해 UI 관심사를 완전히 분리하는 것이었습니다. 웹의 최상단(Root) 레이어에 AnonymousLoginGuard 전역 컴포넌트를 두고, 어디서든 익명 에러가 발생시 신호만 보내면 해당 게이트웨이 컴포넌트가 이를 감지하여 모달을 띄우는 방식입니다.

전반적인 로직에 대한 흐름은 아래와 같습니다.

1단계: 구독(Subscribe)과 발행(Publish) 로직 먼저 간단한 이벤트 핸들링을 위한 중계 레이어를 만들었습니다

// anonymousUtils.ts
let handler: (() => void) | null = null;

export const onRequestAnonymousError = (fn: () => void) => {
  handler = fn;
  return () => { handler = null; };
};

export const requestAnonymousError = () => {
  handler?.();
};

2단계: 전역 가드 컴포넌트 Root에 등록된 해당 컴포넌트는 핸들러를 등록하고 신호를 기다립니다.

// AnonymousLoginGuard.tsx
const AnonymousLoginGuard = () => {
  const [isOpen, setIsOpen] = useState(false);
  const { isAnonymous } = useRecoilValue(authState);

  useEffect(() => {
    // requestAnonymousError() 호출 시 실행될 콜백 등록
    return onRequestAnonymousError(() => {
      if (isAnonymous) setIsOpen(true);
    });
  }, [isAnonymous]);

  return (
    <AnimatePresence>
      {isOpen && (
        <LoginOverlay isOpen={isOpen} onClose={() => setIsOpen(false)} />
      )}
    </AnimatePresence>
  );
};

해당 방식을 통해 API 요청이나 Mutation 단계에서는 “어떻게 모달을 띄울지” 고민할 필요가 없어졌습니다. 단지 에러 발생 시 requestAnonymousError() 한 줄만 호출하면 끝이었죠. 이를 통해 컴포넌트는 모달 상태를 직접 관리하지 않아도 됐고, 동시에 컴포넌트별 중복 코드 또한 제거하며 비즈니스 로직에서 UI를 분리할 수 있었습니다.

하지만 여전히 한계는 있었습니다. 결국 모든 Mutation Hook의 onError 콜백에서 이 함수를 매번 수동으로 호출해줘야 한다는 점이었습니다. 코드는 분리되었지만, 필연적인 보일러 플레이트라는 또 다른 숙제를 남겼습니다.

2차 시도: React Query 전역 핸들러로 자동화하기

수동 호출의 번거로움을 해결하기 위해 이번에는 React Query의 중앙 집중식 에러 핸들링을 이용해 보기로 했습니다. MutationCacheQueryCache를 활용하면 웹 내에서 발생하는 모든 에러를 한곳에서 캐치챌 수 있습니다.

코드 예시

// App.tsx
const queryClient = new QueryClient({
  mutationCache: new MutationCache({
    onError: (error, mutation) => {
      const skipModal = mutation.meta?.skipAnonymousLoginModal;

      if (isAnonymousErrorGuard(error) && !skipModal) {
        requestAnonymousError();
      }
    },
  }),
});

이 설정을 통해 개별 Hook의 코드는 획기적으로 줄어들었습니다. 익명 에러 처리를 위해 작성하던 7줄의 코드가 단 1줄로 줄어들거나 아예 생략이 가능해졌습니다. 또한 meta 필드를 이용해 특정 상황에서는 모달을 띄우지 않는 Escape Hatch도 추가하며 유연성을 더했습니다.

하지만 여기서 또 근본적인 의문이 생겼습니다. “이미 서버에 요청을 보내고 에러를 받은 뒤에 처리하는 게 최선일까?”. 익명 유저임이 확실하면, 애초에 서버로 불필요한 네트워크 요청을 보낼 필요 없이 클라이언트 단에서 먼저 차단하는 것이 효율적이라고 판단했습니다.

최종 솔루션: 하이브리드 방식(사전 차단 + 사후 대응)

최종적으로 도달한 결론은 사전 차단과 사후 대응을 결합한 하이브리드 방식입니다.

1단계: useAuthenticatedMutation을 통한 사전 차단 (Pre-check)

API를 호출하기 전, 클라이언트 상태를 먼저 확인하여 익명 유저라면 요청 자체를 날리지 않고 바로 로그인 모달을 트리거합니다.

export function useAuthenticatedMutation(options) {
  const { isAnonymous } = useRecoilValue(authState);

  return useMutation({
    ...options,
    mutationFn: async (variables) => {
      if (isAnonymous) {
        requestAnonymousError(); // 모달 트리거
        throw new Error("ANONYMOUS_USER_BLOCKED"); // 실행 중단
      }
      return options.mutationFn(variables);
    },
  });
}

2단계: Query 레벨의 enabled 옵션

조회성 API(Query)의 경우 React Query의 enabled: !isAnonymous 조건을 부여하여 불필요한 네트워크 자원 낭비를 방지했습니다.

3단계: 전역 핸들러를 통한 최후 검증

여전히 일반적인 useMutation을 사용하는 곳이 있을 수 있고, 클라이언트와 서버의 인증 상태가 일시적으로 어긋날 수도 있습니다. 이를 위해 앞서 구축한 전역 에러 핸들러를 유지했습니다. 혹시라도 앞단에서 놓친 에러가 있다면, 여기서 마지막으로 한 번 더 잡아내어 사용자 흐름을 지켜냅니다.

결과 및 마이그레이션

이제 팀원들은 로그인이 필요한 기능을 구현할 때 고민할 필요가 없어졌습니다. 단순히 기존의 useMutationuseAuthenticatedMutation으로 한 줄만 바꿔주면 모든 대응이 반영되었습니다.

항목 개선 전 (수동 처리) 개선 후 (하이브리드 방식)
API 호출 에러 발생 시까지 무조건 호출 익명 유저일 경우 호출 전 차단
코드량 Hook마다 익명 체크 로직 필요 Hook 임포트 변경 외 로직 없음
유지보수 누락 가능성 높음 (휴먼 에러) 전역 핸들러가 자동으로 감지
사용자 경험 페이지별로 모달 UI/로직 상이 앱 전역에서 일관된 모달 노출

마치며: 더 나은 DX와 UX를 위한 설계의 가치

이번 설계를 통해 코드의 양을 줄인 것도 기쁘지만, 무엇보다 실수를 하기 어려운 구조를 만들었다는 점에 큰 보람을 느꼈습니다.

특히 일관된 UX는 단순히 ‘예쁜 디자인’의 영역이 아니라 서비스에 대한 유저의 신뢰를 쌓는 영역이라고 생각합니다. 서비스 곳곳에서 제각각으로 동작하던 로그인 유도가 하나로 통합되면서 유저는 서비스의 끊김 없는 흐름을 경험할 수 있게 되었습니다.

설계에 정답은 없겠지만, 우리 팀의 상황에 맞춰 Pub/Sub 패턴에서 시작해 전역 핸들러, 그리고 하이브리드 방식까지 발전시켜 나가는 과정 자체가 더 나은 DX(개발자 경험)와 UX를 고민하는 소중한 시간이었습니다. 비슷한 고민을 하고 계신 분들께 해당 포스팅의 고민 과정이 작은 도움이 되었으면 좋겠습니다 👍🏻