인증 로직 주입은 어디서 해야할까?(feat.횡단관심사분리)

오정진 Jeongjin Oh·2023년 5월 20일
2
post-thumbnail

상황

  • 인증 여부 검증 후 로그인 모달을 띄워야 하는 상황이었습니다.
  • 인증 여부 검증은 특정 이벤트 핸들러나 함수를 실행하기 전에 인증 여부를 검증하고 그 핸들러와 함수를 실행할지 로그인모달을 띄울지 검증하는 것입니다.로그인 여부 검증 다이어그램
  • 이런 검증 로직은 반복되어 분리가 필요했습니다.

문제정의

  • 인증 여부 검증 로직이 반복되므로 이 로직을 분리하는 것으로 문제를 정의했습니다.

해결과정

  • 처음 시도는 특정 이벤트 핸들러를 props 로 넘기는 컴포넌트(children)를 WithAuthHandlers 컴포넌트로 감싸서 그 컴포넌트의 props 에 있는 이벤트 핸들러를 가져와 검증해주는 로직을 추가하는 방식으로 코드를 작성했습니다.

    interface Props {
      handlers: string[];
    }
    
    // 1️⃣ 검증이 필요한 children과 그 children의 handlder 명을 string[]으로 받음
    export const WithAuthHandlers = ({ children, handlers }: PropsWithChildren<Props>) => {
      const { isLogin } = useAuth();
      const modalProps = useModal();
    
      const child = Children.only(children);
      if (!isValidElement(child)) {
        return <>{child}</>;
      }
    
      // 2️⃣ handlers 배열에 있는 핸들러를 인증 여부 검증 로직을 추가함
      const handlerProps = handlers.reduce((acc, cur) => {
        const originHandler = child.props[cur];
        const validateHandler = (...args: unknown[]) => {
          if (!isLogin) modalProps.onOpen();
          else originHandler(...args);
        };
    
        return { ...acc, [cur]: validateHandler };
      }, {});
    
      // 3️⃣ 검증 로직이 추가된 핸들러를 children에 다시 넘겨줌
      // 4️⃣ 로그인 모달(SignUpModal)을 띄울 수 있음
      return (
        <>
          {cloneElement(child, handlerProps)}
          <SignUpModal {...modalProps} />
        </>
      );
    };
    
    // Use case
    <WithAuthHandlers handlers={["onClick"]}>
    	<button onClick={handler}>로그인 여부 검증 버튼</button>
    </WithAuthHandlers>
  • 하지만 위 방식으로 하게 되면 props에 있는 함수만 검증할 수 있게 됩니다. 즉, 아래와 같은 경우는 대응할 수 없습니다.

    <WithAuthHandlers handlers={["onClick"]}>
    	<button onClick={() => {
    		검증_필요_함수();
    		// ❗️onClick 핸들러에 인증 여부와 상관없이 실행해야 하는 함수가 있는 경우 대응할 수 없음
    		인증여부와_상관없이_실행해야하는_함수();
    	}}>
    		로그인 여부 검증 버튼
    	</button>
    </WithAuthHandlers>
  • 위 문제를 해결하기 위해 우리가 필요로 하는 validation 기능이 있는 오픈소스 라이브러리를 탐독했습니다.

  • 그러던 중 react-hook-form 이라는 form validation 을 도와주는 라이브러리를 찾았고, 이 라이브러리의 hook은 어떤 인터페이스를 가지고 있는지 확인하여 저희 프로젝트 코드에 녹이고자 했습니다.

  • 이 라이브러리의 useForm hook을 보며 다음과 같은 해결책을 얻었습니다.

    const { handleSubmit } = useForm();
    const onSubmit = data => {/* submit 전 validation이 필요한 비즈니스 로직 */};
    
    <form onSubmit={handleSubmit(onSubmit)}>
    ...
    </form>
    • handleSubmit 은 form validation을 해주는 함수이고, 이 함수는 onSubmit을 실행하기 전에 validation을 수행합니다.
    • 즉, HOF 패턴을 이용해 onSubmit 핸들러를 validation해주고 있습니다.
  • 로그인 여부 로직을 담당하는 validation 함수를 만들고, 로그인 validation이 필요한 함수나 핸들러를 validation의 인자로 넘겨주는 방식으로 구현할 수 있다는 해결책을 얻었습니다.

  • 그래서 저는 로그인 모달(SignUpModal)을 전역 상태로 다루고, 검증 로직만 hook으로 만들어서 그 hook을 통해 검증 로직 함수를 받아오는 방식으로 구현을 수정했습니다.

    • 해결 PR: PR#113Feat: useAuthValidation hook 구현

      export const useAuthValidation = () => {
        const { isLogin } = useAuth();
      	// 1️⃣ 로그인 모달 상태를 context로 관리
        const modalProps = useSignUpModalContext();
      
      	// 2️⃣ 검증이 필요한 handler를 인자로 받아 인증 로직 수행
        const validate = useCallback(
          <T extends Handler>(handler: T, options: ValidatorOptions = { needSignUpModal: true }) =>
            (...args: Parameters<T>) => {
              if (isLogin) return handler(args) as ReturnType<T>;
              if (!isLogin && options.needSignUpModal) return modalProps.onOpen();
            },
          [isLogin, modalProps],
        );
      
        return { validate };
      };
      
      // Use Case
      const { validate } = useAuthValidation();
      
      const 검증_필요_함수 = () => {/* 로그인 인증이 필요한 로직 */}
      <button onClick={() => {
      	// 😊 인증이 필요한 함수(핸들러)만 validate 함수로 검증 가능
      	validate(검증_필요_함수);
      
      	인증여부와_상관없이_실행해야하는_함수();
      }}>
      	로그인 여부 검증 버튼
      </button>

인사이트

  • 횡단 관심사 분리를 위해 Custom hook, HOF 등 다양한 시도를 해보았습니다.
  • 고도화된 라이브러리 API의 인터페이스를 뜯어보며 비슷한 문제(validation)를 어떤 방식으로 해결하고 있는지 관찰할 수 있습니다.
  • 로직과 컴포넌트의 재사용성과 특정 로직을 분리하기 위한 고민을 프로젝트 팀원들과 이야기하며 하나의 Codebase에 모두가 기여하는 문화를 만들어나갔습니다.
profile
단 한사람의 불편함이라도 해결해 줄 수 있는 개발자가 되고 싶습니다.

0개의 댓글