이제 사진을 담고 리코일 아톰에 보냈으니 input을 관리해보자.
구현할 구역은 다음과 같다.
위에 맞게 기획해보자면
1. 이메일 전송 및 인증번호 확인 input/API 연결 및 유효성 검사
2. 비밀번호 및 확인 input 및 유효성 검사
3. 실명-이름 input 및 유효성 검사
4. 닉네임 input/API 연결 및 유효성 검사
진짜 겁나 길다; 300줄 이상이라 그렇다. 보기 싫다면 넘어가는걸 추천한다.
벨로그는 드롭다운이 왜 없나몰라
import React, { useCallback, useEffect, useState } from "react";
import { Text, Margin } from "@/src/components/ui";
import styled from "styled-components";
import Image from "next/image";
interface StyledInputProps {
small: boolean;
error: boolean;
}
interface InputData {
email: string;
emailCertificaion: string;
password: string;
name: string;
nickname: string;
}
function ErrorMent({ error, errorMent, ment }: any) {
return (
<>
{error ? (
<>
<S.ErrorWrapper>
<Image src="/auth/error.svg" alt="error" />
<Margin direction="row" size={8} />
<Text.Caption3 color="red500">{errorMent}</Text.Caption3>
</S.ErrorWrapper>
</>
) : (
<Text.Caption3 color="gray900">{ment}</Text.Caption3>
)}
</>
);
}
export default function InputBox({ getError }: { getError: (error: boolean) => void }) {
const [buttonMessage, setButtonMessage] = useState("인증 전송");
const [emailMent, setEmailMent] = useState("");
const [inputData, setInputData] = useState<InputData>({
email: "",
emailCertificaion: "",
password: "",
name: "",
nickname: "",
});
const { email, emailCertificaion, password, name, nickname } = inputData;
const [isError, setError] = useState(true);
const [isErrorEmailCertify, setErrorEmailCertify] = useState(false);
const [isErrorPassword, setErrorPassword] = useState(false);
const [isErrorPasswordCheck, setErrorPasswordCheck] = useState(false);
const [isErrorName, setErrorName] = useState(false);
const [isErrorNickName, setErrorNickName] = useState(false);
const handleInputData = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setInputData((prevData) => ({ ...prevData, [e.target.name]: e.target.value }));
}, []);
const handleInputError = useCallback(() => {
if (
!isErrorEmailCertify &&
!isErrorPassword &&
!isErrorPasswordCheck &&
!isErrorName &&
!isErrorNickName
) {
setError(false);
} else {
setError(true);
}
}, [isErrorEmailCertify, isErrorPassword, isErrorPasswordCheck, isErrorName, isErrorNickName]);
const handleEmail = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{6,15}$/;
if (!passwordRegex.test(e.target.value)) {
setErrorPassword(true);
} else {
setErrorPassword(false);
}
}, []);
const handlePassword = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{6,15}$/;
if (!passwordRegex.test(e.target.value)) {
setErrorPassword(true);
} else {
setErrorPassword(false);
}
}, []);
const handleName = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const special_pattern = /[`~!@#$%^&*|\\\'\";:\/?]/gi;
if (special_pattern.test(e.target.value)) {
setErrorName(true);
} else {
setErrorName(false);
}
}, []);
const handleNickName = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const pattern = /[`~!@#$%^&*|\\\'\";:\/?]/;
const pattern2 = /[0-9]/;
if (
pattern.test(e.target.value) ||
pattern2.test(e.target.value) ||
e.target.value.length < 2 ||
e.target.value.length > 10
) {
setErrorNickName(true);
} else {
setErrorNickName(false);
}
}, []);
useEffect(() => {
handleInputError();
getError(isError);
}, [isError, handleInputError, getError]);
return (
<>
<Text.Body1 color="gray700">이메일</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
<S.Content>
<S.InputField small={true} error={false}>
<input
name="Email"
value={email}
type="email"
placeholder="abc@naver.com"
onChange={(e) => {
handleInputData(e);
handleEmail(e);
}}
/>
</S.InputField>
<S.ButtonWrapper small={true} error={false}>
<S.Button
onClick={() => {
setButtonMessage("재전송");
setEmailMent("인증메일을 전송했습니다.");
}}
>
{buttonMessage}
</S.Button>
</S.ButtonWrapper>
</S.Content>
<ErrorMent error={false} errorMent="" ment={emailMent} />
</S.ContentWrapper>
<Text.Body1 color="gray700">인증번호 입력</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
<S.Content>
<S.InputField small={true} error={isErrorEmailCertify}>
<input
name="EmailCertificaion"
value={emailCertificaion}
type="string"
placeholder="인증번호"
onChange={handleInputData}
/>
</S.InputField>
<S.ButtonWrapper small={true} error={isErrorEmailCertify}>
<S.Button
onClick={() => {
inputData.emailCertificaion === "123"
? setErrorEmailCertify(false)
: setErrorEmailCertify(true);
}}
>
인증 확인
</S.Button>
</S.ButtonWrapper>
</S.Content>
<ErrorMent error={isErrorEmailCertify} errorMent="인증정보가 일치하지 않습니다." ment=" " />
</S.ContentWrapper>
<Text.Body1 color="gray700">비밀번호</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
<S.Content>
<S.InputField small={false} error={isErrorPassword}>
<input
name="Password"
value={password}
type="string"
placeholder="비밀번호"
onChange={(e) => {
handleInputData(e);
handlePassword(e);
}}
/>
</S.InputField>
<S.ButtonWrapper small={false} error={false}></S.ButtonWrapper>
</S.Content>
<ErrorMent
error={isErrorPassword}
errorMent="영문자/숫자/특수기호 포함 최소 8자, 최대 15자 "
ment="영문자/숫자/특수기호 포함 최소 8자, 최대 15자"
/>
</S.ContentWrapper>
<Text.Body1 color="gray700">비밀번호 확인</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
<S.Content>
<S.InputField small={false} error={false}>
<input
type="string"
placeholder="비밀번호"
onChange={(e) => {
e.target.value === inputData.password
? setErrorPasswordCheck(false)
: setErrorPasswordCheck(true);
}}
/>
</S.InputField>
<S.ButtonWrapper small={false} error={false}></S.ButtonWrapper>
</S.Content>
<ErrorMent error={isErrorPasswordCheck} errorMent="정보를 정확히 입력해주세요." ment=" " />
</S.ContentWrapper>
<Text.Body1 color="gray700">이름</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
<S.Content>
<S.InputField small={false} error={false}>
<input
name="Name"
value={name}
type="string"
placeholder="김윙글"
onChange={(e) => {
handleInputData(e);
handleName(e);
}}
/>
</S.InputField>
<S.ButtonWrapper small={false} error={false}></S.ButtonWrapper>
</S.Content>
<ErrorMent
error={isErrorName}
errorMent="실명을 입력하세요 (한글, 영어 대/소문자 사용 가능) "
ment=" 실명을 입력하세요 (한글, 영어 대/소문자 사용 가능) "
/>
</S.ContentWrapper>
<Text.Body1 color="gray700">닉네임</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
<S.Content>
<S.InputField small={true} error={false}>
<input
name="NickName"
value={nickname}
type="string"
placeholder="희망찬윙그리"
onChange={(e) => {
handleInputData(e);
handleNickName(e);
}}
/>
</S.InputField>
<S.ButtonWrapper small={true} error={false}>
<S.Button>중복 확인</S.Button>
</S.ButtonWrapper>
</S.Content>
<ErrorMent
error={isErrorNickName}
errorMent="한글/영어 두글자 이상 10글자 이하 "
ment=" "
/>
</S.ContentWrapper>
</>
);
}
const S = {
ContentWrapper: styled.div`
padding-bottom: 24px;
`,
Content: styled.div`
display: flex;
`,
InputField: styled.div<StyledInputProps>`
width: ${(props) => (props.small ? "345px" : "452px")};
height: 50px;
border: ${(props) => (props.error ? "1px solid #FF7070" : "1px solid #dcdce0;")};
border-radius: 8px;
margin-bottom: 8px;
& > input {
width: 300px;
border: none;
padding: 14px;
border-radius: 8px;
height: 22px;
&::placeholder {
font-weight: 400;
font-size: 16px;
line-height: 140%;
color: #959599;
}
}
`,
Button: styled.button`
font-size: 16px;
font-weight: 700;
color: #49494d;
width: 99px;
`,
ButtonWrapper: styled.div<StyledInputProps>`
display: ${(props) => (props.small ? "flex" : "none")};
height: 50px;
width: 99px;
border: 1px solid #959599;
border-radius: 8px;
margin-left: 8px;
`,
ErrorWrapper: styled.div`
display: flex;
`,
};
buttonMessage
, emailMent
등으로 에러 시 나올 메세지나 완료시 메시지를 넣으려고 했고, inputData
로 데이터를 따로 관리했다.
불린데이터인 isError~
를 넣어 에러가 일어나면 멘트가 나오는 등의 조건을 만들었다.
나머지 handle~
는 거의 유효성 검사를 해서 isError~
의 set에 영향을 줘 에러처리를 했다.
위에 적혀있는 ErrorMent
는 따로 컴포넌트로 옮겼다. 옮기기만 한 것이기에 따로 적진 않았다.
위처럼 구획을 나눠서 기능 개발을 할 것이다. 이제 스타트 해보자!
윙글은 이메일을 아이디로 쓰일 것이기 때문에 이메일이 중복되는가 안되는가를 확인하는 동시에 입력한 이메일에 메일을 보내 인증번호를 받아오고, API 통신 코드를 구현하여 인증번호를 입력해 서버에서 인증을 받아오는 식이다.
먼저 API 코드를 구현해보자.
import instance from "../axiosModul";
interface EmailAuthResponse {
status: number;
message: string;
data: {
certificationKey?: string;
};
}
export const sendEmailAuth = async (email: string): Promise<EmailAuthResponse> => {
const response = await instance.post<EmailAuthResponse>("/auth/email", { email: email });
return response.data;
};
되게 쉽다.
sendEmailAuth
함수를 선언하고 문자열인 email
을 파라미터로 받는다. instance
를 가져와 email
을 바디로 보내며 POST한다.이게 끝이다! EmailAuthResponse
타입도 선언해줘 안전하게 데이터를 받고 보냈다.
다 끝났다! sendEmailAuth
함수를 가져와 인자로 email
을 넣어준다. 그다음 mutate
하는 함수 sendEmail
을 가져와 호출을 하면 된다.
handleSendEmail
을 만들어 sendEmail
를 호출해준다. email
상태가 ""
, 즉 입력하지 않았다면 안되게 설정도 해놨다!
// 이메일 인증메일 보내기
const { mutate: sendEmail } = useMutation(() => sendEmailAuth(email));
const handleSendEmail = useCallback(() => {
if (email === "") {
alert("이메일을 입력해주세요.");
return;
}
sendEmail();
}, [email, sendEmail]);
이제 여기에 리액트 쿼리의 진행 상태에 따라 실행되는 함수도 넣어줄 수 있다.
const { mutate: sendEmail } = useMutation(() => sendEmailAuth(email), {
onMutate: () => {
setButtonMessage("전송 중");
},
onSuccess: () => {
setButtonMessage("재전송");
setEmailMent("인증메일을 전송했습니다.");
},
onError: (error) => {
setErrorEmailCertify(true);
alert(error);
throw error;
},
});
onMutate
: sendEmail
이 실행 전 보내기 버튼의 value
인 메세지에 '전송 중'을 보냈다.onSuccess
: sendEmail
이 실행 후 응답이 성공했다면(200) 버튼 메세지를 '재전송'으로 바꾸고, setEmailMent
을 만들어 input 아래 멘트에 나오도록 했다.onError
: sendEmail
이 실행 후 실패했다면 setErrorEmailCertify
이 true
, 즉 에러가 떴다고 상태를 바꾼다.먼저 알아둬야할게 handleEmail
이라는 유효성 검사맡는 함수가 사라지고 그냥 handleInputData
를 넣었다. 나중에 인증번호가 오고, 맞게 입력한다면 그때 리코일 아톰에 들어가기로 했다. 걍 유효성 검사가 필요 없어서 뺐다.
<Text.Body1 color="gray700">이메일</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
<S.Content>
<S.InputField small={true} error={false}>
<input
name="email"
value={email}
type="email"
placeholder="abc@naver.com"
onChange={(e) => {
handleInputData(e);
}}
/>
</S.InputField>
<S.ButtonWrapper small={true} error={false}>
<S.Button onClick={() => handleSendEmail()}>{buttonMessage}</S.Button>
</S.ButtonWrapper>
</S.Content>
<ErrorMent error={false} errorMent="" ment={emailMent} />
</S.ContentWrapper>
입력할 때마다 handleInputData
이 실행, 입력 후 버튼을 누르면 handleSendEmail
를 실행한다. 그 후로 API가 실행되어 이메일이 받아와진다.
다음편엔 이어서 이메일 검증, 비밀번호, 닉네임을 후술하겠다. 이게 은근 노가다성이라서 빡셌다.
고생하셨네요.. 디자인은 300줄 이상이라 스킵했습니다.. ㅎㅎ
React Query를 잘활용하시는거 같네요. 화이팅입니다!