초창기 프론트엔드라고 하면 단순 뷰 로직 정도만 구현하여 프론트엔드는 큰 역할을 못하는 것처럼 인식이 되었습니다. 하지만 현재 많은 기술스택들이 생겨나고 이로 인해 프론트엔드의 역할은 점점 커지고 있습니다.
기존 mvc아키텍쳐에서 mvp 그리고 mvvp 등 많은 아키텍쳐들이 생겨나고 있습니다. 지금도 여기서 그치지 않고 더 효율적인 코드를 작성하기 위해 많은 프론트엔드 개발자들은 아키텍쳐를 만들고 적용해나가고 있습니다. 그 설계에 대한 핵심은 비즈니스 로직과 뷰를 분리
라고 생각이 됩니다.
일단 이를 이해하기 위해 비즈니스 로직과 뷰에 대한 간단한 정의를 찾아봅시다.
비즈니스 로직 : 애플리케이션의 핵심 기능과 데이터를 처리(상태관리 라이브러리, API콜 등)
뷰 : 사용자에게 보이는 인터페이스 부분(UI/UX)
간단한 예시를 들어보자면, 사용자가 로그인을 하기 위해 입력하고 입력값에 대한 api의 post콜과 같은 경우는 비즈니스 로직, 그 결과에 따른 로그인 성공 실패 ui를 보여주는 것이 뷰라고 할 수 있겠습니다.
/**로그인 컴포넌트*/
//...import module생략...
const UserLogin = () => {
/**useHookform으로 form관리*/
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginData>();
/**react-query로 API post요청(로그인 요청)*/
const queryClient: QueryClient = useQueryClient();
const { mutateAsync: loginHandler, isLoading: loginLoading } = useMutation(
(loginData: LoginData) => postLogin(loginData),
{
onError: () => {
toast("ID또는 Password가 틀렸습니다.");
},
onSuccess: () => {
queryClient.invalidateQueries(["me"]);
toast("로그인 성공!!");
},
}
);
/**로그인 form을 제출했을 때*/
const onSubmit = async (loginData: LoginData) => {
await loginHandler(loginData);
};
return (
<form
onSubmit={handleSubmit(onSubmit)}>
<input
placeholder="UserEmail"
{...register("email", {
required: "ID를 입력해주세요.",
})}
/>
<input
placeholder="Password"
type="password"
autoComplete="off"
{...register("password", {
required: "Password를 입력해주세요.",
})}
/>
<div
{(errors.email && (
<p>⚠ {errors.email.message}</p>
)) ||
(errors.password && (
<p>⚠ {errors.password.message}</p>
))}
<div>
<GoHomeBtn />
<Button
type="submit"
로그인
</Button>
</div>
</div>
</form>
);
};
export default UserLogin;
다음은 제가 프로젝트 기간 작성했던 코드입니다. 코드에서 확인 할 수 있듯이 UserLogin컴포넌트 내부에 API콜을 처리하는 비즈니스로직과 여러 함수 그리고 뷰가 모두 담겨져 있는 것을 확인 할 수 있습니다.
만약 API콜의 로직을 수정해야한다면 하나의 컴포넌트안에서 로직을 찾기 위해 큰 수고를 가져올 수 있습니다.
const LoginPage = () =>{
/**react-query로 API post요청(로그인 요청)*/
const queryClient: QueryClient = useQueryClient();
const { mutateAsync: loginHandler, isLoading: loginLoading } = useMutation(
(loginData: LoginData) => postLogin(loginData),
{
onError: () => {
toast("ID또는 Password가 틀렸습니다.");
},
onSuccess: () => {
queryClient.invalidateQueries(["me"]);
toast("로그인 성공!!");
},
}
);
/**로그인 form을 제출했을 때*/
const onSubmit = async (loginData: LoginData) => {
await loginHandler(loginData);
};
return (<main>
<UserLogin onSubmit={onSubmit} loginLoading={loginLoading}>
</main>)
}
export default LoginPage
위의 코드는 기존의 UserLogin 컴포넌트에서의 비즈니스 로직을 상위 컴포넌트인 LoginPage에서 해결하고 props로 보내줍니다. 비즈니스 로직과 뷰과 확실하게 분리되어 유지보수하기가 쉬워졌습니다. 하지만 문제가 발생합니다. 컴포넌트 설계에 따라 복잡해지게 된다면 여러 컴포넌트에 걸쳐서 props drilling문제가 발생하게 됩니다. 결국 비즈니스로직을 받는 컴포넌트의 의존성 또한 커질 수 있게 됩니다.
/**useLogin 커스텀훅*/
const useLogin = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginData>();
/**react-query로 API post요청(로그인 요청)*/
const queryClient: QueryClient = useQueryClient();
const { mutateAsync: loginHandler, isLoading: loginLoading } = useMutation(
(loginData: LoginData) => postLogin(loginData),
{
onError: () => {
toast("ID또는 Password가 틀렸습니다.");
},
onSuccess: () => {
queryClient.invalidateQueries(["me"]);
toast("로그인 성공!!");
},
}
);
/**로그인 form을 제출했을 때*/
const onSubmit = async (loginData: LoginData) => {
await loginHandler(loginData);
};
return {handleSubmit, register, errors, onSubmit, loginLoading };
};
export default useLogin;
/**로그인 컴포넌트*/
//...import module생략...
const UserLogin = () => {
const router = useRouter();
const { handleSubmit, register, errors, onSubmit, loginLoading } = useLogin();
return (
<form
onSubmit={handleSubmit(onSubmit)}>
//...생략
</form>)
};
export default UserLogin;
useLogin 커스텀 훅을 만들어 기존의 비즈니스로직을 모두 숨겨 구조 분해 할당을 통해 뷰와 연결 시켜주었습니다. props drilling문제를 해결할 뿐만 아니라 좀 더 직관적인 코드가 완성되었습니다. 하지만 간과하고 있는 점이 있습니다. 비즈로직을 완벽하게 숨김으로써 클린한 코드이냐?
라는 점입니다.
위의 커스텀 훅을 사용한 방식에는 한 가지 문제가 있습니다. UserLogin에서 useLogin 커스텀훅에서 가져온 함수들은 정확히 무슨 역할을 하는 함수인지 알아보기가 힘든 것입니다. 좋은 코드를 작성한다는 것은 작성자 뿐만아니라 다른 사람도 충분히 알기 쉽게 되어있어야 합니다.
그러면 위의 코드를 어떻게 바꿔야 할까요?? 디테일한 로직은 확실히 커스텀 훅에 숨기며 jsx와 직접적으로 연결된 함수같은 것은 보여지는 편이 더 좋을 것 같습니다.
const UserLogin = () => {
const { handleSubmit, loginLoading } = useLogin();
/**useHookform으로 form관리*/
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginData>();
/**로그인 form을 제출했을 때*/
const onSubmit = async (loginData: LoginData) => {
await loginHandler(loginData);
};
return (
<form
onSubmit={handleSubmit(onSubmit)}>
//...생략
</form>)
}
export default UserLogin
기존의 커스텀훅에서 숨겼던 onSubmit함수와 useForm을 꺼내고 세부적인 로직은 그대로 숨겨두었습니다. 핵심적인 부분 (액션, error, 함수)을 공개하여 보다 코드를 직관적으로 나타낼 수 있습니다.
사실 react에서 custom hook의 용도는 반복되는 작업을 간소화 하기 위함입니다. custom hook을 비즈니스 로직을 숨기는 용도로 사용하게 되면 반복되는 작업을 피하는 본래의 목적에 어긋나는 작업
을 한 것이라고도 볼 수 있습니다. 이 때문에 비즈니스 로직을 커스텀 훅에 분리하는 것에 대한 회의적인 의견도 찾아 볼 수 있었습니다.
프론트엔드의 급진적인 발전으로 인해 best practice
는 아직까지 명쾌하지 않으며, 어느 방법이 딱 정답이란 것이 없는 것 처럼 보입니다. 따라서 상황에 맞는 방법으로 해결하는 것이 좋은 방법인 것 같습니다. (아직까지 custom Hook을 사용한 방법이 좋아보임)
Reference
https://blog.leehov.in/57
https://www.youtube.com/watch?v=edWbHp_k_9Y&t=632s
https://velog.io/@sonwanseo/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC