지난번 회고(1)에서 적었던, 로직을 구현 뒤 모달 컴포넌트, input등을 활용하여서 로그인과 회원가입을 구현하였다. 따라서 이번에 회고하게 될 주요한 사항들은 UI 컴포넌트와, react-hook-form의 사용이다.
로그인과 회원가입의 경우 Navigation 컴포넌트 내에서 해당하는 버튼을 누르면 모달이 등장하게끔 기획하였다. 따라서 input을 만들기 전에 Navigation 컴포넌트를 만들어 주었고, Navigation 컴포넌트도 만들어 주었다.
function Modal({ children, width, height, visible, onClose, ...props }) {
// custom hooks
const ref = useClickAway(() => {
if (onClose) onClose();
});
const containerStyle = useMemo(() => ({
width,
height,
}));
const elem = useMemo(() => document.createElement("div"), []);
useEffect(() => {
document.body.appendChild(elem);
return () => {
document.body.removeChild(elem);
};
});
return ReactDOM.createPortal(
<Ms.BackgroundDim style={{ display: visible ? "block" : "none" }}>
<Ms.ModalContainer
ref={ref}
style={{ ...props.style, ...containerStyle }}
>
{children}
</Ms.ModalContainer>
</Ms.BackgroundDim>,
elem
);
}
먼저 효과적인 모달 구현을 위해서 React의 portal을 사용하였다. 공식문서
portal은 DOM의 다른 위치에 컴포넌트를 삽입하고자 할때 사용하면 되는데 위의 모달의 경우 body 아래의 child로 사용하고자 portal을 사용하게 되었다. (이 부분은 useEffect 부분의 코드를 보면 보다 이해하기 쉬울 것이다). 또한 onClose 함수를 받아서 만약 dim, modal 뒤쪽의 까만 배경을 클릭했을 경우 onClose 함수를 실행하게끔 구현하였다.
const events = ["mousedown", "touchstart"];
function useClickAway(handler) {
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const handleEvent = (e) => {
if (!element.contains(e.target)) handler();
};
events.forEach((event) => document.addEventListener(event, handleEvent));
// eslint-disable-next-line
return () => {
events.forEach((event) =>
document.removeEventListener(event, handleEvent)
);
};
}, [ref]);
return ref;
}
이 custom hooks는 dim 클릭시 모달을 닫게 하게끔 하기 위한 customhooks이다. 따라서 handler 함수를 props로 받았고, 만약 클릭한 지점이 element에 포함되어 있지 않으면 handler 함수를 실행하게끔 구현하였다.
모달이 두개이지만, 모달을 open할 때 사용하는 state는 두개 사용하지 않고 object로 관리 하였다.
const [modalStatus, setModalStatus] = useState({
visible: false,
type: "", //login, signup
});
또한 로그인시, 로그인 상태가 아닌지에 따라 navigation 컴포넌트의 렌더링 내용이 달라져야 하기 때문에 loginBlock과 logoutBlock으로 나누었다. (이전 회고에서 작성한 것 처럼 로그인 여부를 전역 state로 관리하였기 때문에 이 여부에 따라 각기 다른 block 컴포넌트를 렌더링 해주었다.)
const [isLogined, setIsLogined] = useRecoilState(loginStatus);
Login과 SignUp 컴포넌트를 구현하기 앞서, 팀원 분께서 Input 컴포넌트를 완성시켜 주셨다.
따라서 위 두개의 컴포넌트를 구현할 때 이 Input 컴포넌트를 활용하기로 하였다.
처음에 이 Input 컴포넌트에 다음과 같이 바로 react-hook-form 코드를 작성하였지만, react-hook-form이 제대로 작동하지 않았다.
// ❌ 잘못된 예시(바로 사용하면 안됨)
<Input {...register("field")} />
다만 공식문서와 구글링을 통해서 이에 대한 해법을 찾을 수 있었다.
react-hook-form같은 경우, component에 이를 적용하고 싶으면 forwardRef를 통해서 ref를 컴포넌트에 전달해주어야 한다.
// 예시
const Input = React.forwardRef((props, ref) => {
// ...componet 로직
return (
<input {...props} ref={ref} />
);
});
따라서 위와 같은 방법으로 Input 컴포넌트를 변경하고 나서야 제대로 작동되었다.
form 컨트롤을 위해서 react-hook-form에서 useForm을 가져왔고, 여기서 register와 handleSubmit, formState에서 errors를 가져왔다.
// 예시
import { useForm } from "react-hook-form";
const Login = (props) => {
//input이 들어오는 것을 감지하여 error 메세지를 출력하기 위해서 mode를 all로 설정하였다.
const { register, handleSubmit, formState: { errors } } = useForm({ mode: "all" });
// login 로직들....
//submit 처리
const handleSubmit = async (data) => {
const { id, password } = data;
const result = await loginFetchFunction(id, password);
};
return (
<form>
<Input {...register("id", { required: "필수" })} />
// optional chaining을 넣어야 typeerror가 발생하지 않는다
{errors?.id?.message}
<Input {...register("password", { required: "필수", ...other rules })} type="password" />
{errors?.password?.message}
<button type="submit">로그인</button>
</form>
);
};
SignUp컴포넌트 또한 위와 유사하게 구현 하였다. 다만, 회원가입의 경우, 로그인에 비해 더 많은 검증요소가 필요하기 때문에 이를 작성해 주었다.
<input {...register("someInput", maxLength: { value: 12, message: "최대 12자리" })} />
react-hook-form을 사용하다가 error message 지정을 어떻게 해야할지 모르겠다면, 위와 같이 value에는 값, message에 메세지를 지정하여 주면 된다.
| 로그인 모습 | 회원가입 모습 |
|---|---|
![]() | ![]() |