구현
먼저 구현하기 위해 간단한 핸드폰 번호 입력 폼과 전송 버튼을 간단하게 만들어보았다. 기능에 초점을 두기 위해 최대한 디자인은 심플하게 구성하였다.
import styled from "styled-components";
export default function Verification() {
return (
<Wrapper>
<Form.Wrapper>
<Form.Input
placeholder="01012345678"
maxLength={11}
type="text"
/>
<Form.Button>인증번호 전송</PhoneNumber.Button>
</Form.Wrapper>
</Wrapper>
);
}
const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
const Form = {
Wrapper: styled.div`
display: flex;
gap: 8px;
`,
Input: styled.input`
padding: 12px 18px;
border: none;
border-radius: 8px;
background-color: #f5f5f5;
&::placeholder {
color: #bebebe;
}
`,
Button: styled.button`
background: #1273e4;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
width: 100px;
&:hover {
background-color: #105cb6;
}
&:active {
background-color: #0e4a9b;
transform: scale(0.98);
}
`,
};
핸드폰 입력 폼 구성
구현할 기능
- 핸드폰 입력 폼에 숫자만 입력하게 설정
- 입력 시 숫자 길이에 따라 하이픈(-) 넣기
핸드폰 번호는 하이푼 없이 관리하기 위해 보여주는 부분과 번호 관리하는 부분을 따로 관리하였다.
const [phoneNumber, setPhoneNumber] = useState("");
// 입력 폼 변경 시 실행되는 함수
const handleChange = (e: { target: { value: string } }) => {
const numbersOnly = e.target.value.replace(/\D/g, "");
if (numbersOnly.length <= 11) {
setPhoneNumber(numbersOnly);
}
};
// 전화번호 입력에 '-' 넣는 함수
const displayFormattedPhoneNumber = (numbers: string) => {
if (numbers.length <= 3) {
return numbers;
} else if (numbers.length <= 7) {
return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
} else {
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(
7
)}`;
}
};
<Form.Input
placeholder="01012345678"
type="text"
value={displayFormattedPhoneNumber(phoneNumber)}
onChange={handleChange}
/>
인증번호 입력 UI 구현
구현할 기능
- 인증번호 전송을 누르면 하단에 인증번호를 입력 UI 띄우기
const [verificationSent, setVerificationSent] = useState(false);
const [verificationCode, setVerificationCode] = useState("");
// 인증번호 전송 함수
const sendVerification = () => {
if (phoneNumber.length < 10) return;
setVerificationSent(true);
// 인증번호 전송하는 로직
};
// 인증번호 재전송 함수
const resendVerification = () => {
// 인증번호 재전송하는 로직
};
// 인증번호 확인 함수
const verifyCode = () => {
// 인증번호 확인하는 로직
};
<Form.Wrapper>
<Form.Input
placeholder="01012345678"
type="text"
value={displayFormattedPhoneNumber(phoneNumber)}
onChange={handleChange}
/>
<Form.Button
onClick={!verificationSent ? sendVerification : resendVerification}
>
{!verificationSent ? "인증번호 전송" : "재전송"}
</Form.Button>
</Form.Wrapper>
{verificationSent && (
<Form.Wrapper>
<Form.Input
placeholder="인증번호 입력"
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
/>
<Form.Button onClick={verifyCode}>
확인
</Form.Button>
</Form.Wrapper>
)}
타이머 기능 구현
구현할 기능
- 인증번호 전송 버튼 누르면 타이머 보여주기
- 재전송 버튼 누르면 타이머 초기화
const [timer, setTimer] = useState(180);
// 인증번호 재전송 함수
const resendVerification = () => {
setTimer(180);
};
//타이머 함수
const formatTime = () => {
const minutes = Math.floor(timer / 60);
const seconds = timer % 60;
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
};
useEffect(() => {
let interval: NodeJS.Timeout;
if (verificationSent) {
interval = setInterval(() => {
setTimer((prevTimer) => {
if (prevTimer <= 1) {
clearInterval(interval);
setVerificationSent(false);
return 0;
}
return prevTimer - 1;
});
}, 1000);
}
return () => clearInterval(interval);
}, [verificationSent]);
{verificationSent && (
<Form.Wrapper>
<Form.Timer>{formatTime()}</Form.Timer>
<Form.Input
placeholder="인증번호 입력"
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
/>
<Form.Button onClick={verifyCode}>확인</Form.Button>
</Form.Wrapper>
)}
const Form = {
...
Timer: styled.div`
position: absolute;
right: 120px;
top: 50%;
transform: translate(0, -50%);
color: #1273e4;
font-weight: bold;
`,
}
인증번호 기능 구현
구현할 기능
- 인증번호 숫자만 입력, 6글자까지 제한
- 확인을 눌러 인증번호 검증하기
- 인증번호가 틀리다면 경고 문구를 띄우기
- 인증번호가 맞다면 성공 문구를 띄우기
const [isVerified, setIsVerified] = useState(false);
const [verificationMessage, setVerificationMessage] = useState("");
// 인증번호 확인 함수
const verifyCode = () => {
if (verificationCode === "111111" && timer > 0) {
setIsVerified(true);
setVerificationMessage("인증 성공");
} else {
setIsVerified(false);
setVerificationMessage("인증번호가 잘못되었습니다.");
}
};
// 인증번호 입력 변경 함수
const handleVerificationCodeChange = (e: { target: { value: string } }) => {
const numbersOnly = e.target.value.replace(/\D/g, "");
if (numbersOnly.length <= 6) {
setVerificationCode(numbersOnly);
}
};
{verificationSent && (
<Form.Wrapper>
<Form.Timer>{formatTime()}</Form.Timer>
<Form.Input
placeholder="인증번호 입력"
type="text"
value={verificationCode}
onChange={handleVerificationCodeChange}
/>
<Form.Button onClick={verifyCode} disabled={isVerified}>
확인
</Form.Button>
{!isVerified && (
<Form.Message color="red">{verificationMessage}</Form.Message> // 인증 실패 메시지
)}
{isVerified && (
<Form.Message color="green">{verificationMessage}</Form.Message> // 인증 성공 메시지
)}
</Form.Wrapper>
)}
const Form = {
...
Message: styled.div`
color: ${(props) => props.color};
position: absolute;
left: 10px;
bottom: -30px;
`,
}
리팩토링
만들고 나니 .. 리팩토링이 필요한 부분이 있어 코드를 수정하였다.
1. 매직넘버 따로 관리하기
입력 길이 제한, 타이머 초 등은 따로 상수로 관리하였다.
const INITIAL_TIMER_SECONDS = 180;
const PHONE_NUMBER_LENGTH = 11;
const VERIFICATION_CODE_LENGTH = 6;
2. 인증 상태를 한번에 관리
인증번호 전송, 검증 상태를 한번에 관리했다.
const [verificationStatus, setVerificationStatus] = useState({
sent: false,
verified: false,
message: "",
});
// 인증번호 전송 함수
const sendVerification = () => {
if (phoneNumber.length < 10) return;
setVerificationStatus((prevStatus) => ({ ...prevStatus, sent: true }));
};
// 인증번호 확인 함수
const verifyCode = () => {
if (verificationCode === "111111") {
setVerificationStatus({
sent: true,
verified: true,
message: "인증 성공",
});
} else {
setVerificationStatus({
sent: true,
verified: false,
message: "인증번호가 잘못되었습니다.",
});
}
};
<Form.Wrapper>
<Form.Input
placeholder="01012345678"
type="text"
value={displayFormattedPhoneNumber(phoneNumber)}
onChange={handleChange}
/>
<Form.Button
onClick={
!verificationStatus.sent ? sendVerification : resendVerification
}
>
{!verificationStatus.sent ? "인증번호 전송" : "재전송"}
</Form.Button>
</Form.Wrapper>
{verificationStatus.sent && (
<Form.Wrapper>
<Form.Timer>{formatTime()}</Form.Timer>
<Form.Input
placeholder="인증번호 입력"
type="text"
value={verificationCode}
onChange={handleVerificationCodeChange}
/>
<Form.Button
onClick={verifyCode}
disabled={verificationStatus.verified}
>
확인
</Form.Button>
{!verificationStatus.verified && (
<Form.Message color="red">
{verificationStatus.message}
</Form.Message> // 인증 실패 메시지
)}
{verificationStatus.verified && (
<Form.Message color="green">
{verificationStatus.message}
</Form.Message> // 인증 성공 메시지
)}
</Form.Wrapper>
)}
3. 커스텀 훅으로 타이머 로직 분리
function useTimer(initialSeconds: number) {
const [timer, setTimer] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval: NodeJS.Timeout;
if (isActive) {
interval = setInterval(() => {
setTimer((prevTimer: number) => (prevTimer > 0 ? prevTimer - 1 : 0));
}, 1000);
}
return () => clearInterval(interval);
}, [isActive]);
const resetTimer = () => {
setTimer(initialSeconds);
setIsActive(true);
};
const stopTimer = () => {
setIsActive(false);
};
return { timer, resetTimer, stopTimer };
}
const { timer, resetTimer, stopTimer } = useTimer(INITIAL_TIMER_SECONDS);
// 인증번호 재전송 함수
const resendVerification = () => {
setVerificationStatus((prevStatus) => ({
...prevStatus,
verified: false,
message: "",
}));
setVerificationCode("");
resetTimer();
};
useEffect(() => {
if (verificationStatus.sent) {
resetTimer();
}
}, [verificationStatus.sent]);
useEffect(() => {
if (verificationStatus.verified) {
stopTimer();
}
}, [verificationStatus.verified]);
완성
구현결과
전체코드
import { useEffect, useState } from "react";
import styled from "styled-components";
const INITIAL_TIMER_SECONDS = 180;
const PHONE_NUMBER_LENGTH = 11;
const VERIFICATION_CODE_LENGTH = 6;
function useTimer(initialSeconds: number) {
const [timer, setTimer] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval: NodeJS.Timeout;
if (isActive) {
interval = setInterval(() => {
setTimer((prevTimer: number) => (prevTimer > 0 ? prevTimer - 1 : 0));
}, 1000);
}
return () => clearInterval(interval);
}, [isActive]);
const resetTimer = () => {
setTimer(initialSeconds);
setIsActive(true);
};
const stopTimer = () => {
setIsActive(false);
};
return { timer, resetTimer, stopTimer };
}
export default function Verification() {
const [phoneNumber, setPhoneNumber] = useState("");
const [verificationCode, setVerificationCode] = useState("");
const [verificationStatus, setVerificationStatus] = useState({
sent: false,
verified: false,
message: "",
});
const { timer, resetTimer, stopTimer } = useTimer(INITIAL_TIMER_SECONDS);
// 입력 폼 변경 시 실행되는 함수
const handleChange = (e: { target: { value: string } }) => {
const numbersOnly = e.target.value.replace(/\D/g, "");
if (numbersOnly.length <= PHONE_NUMBER_LENGTH) {
setPhoneNumber(numbersOnly);
}
};
// 전화번호 입력에 '-' 넣는 함수
const displayFormattedPhoneNumber = (numbers: string) => {
if (numbers.length <= 3) {
return numbers;
} else if (numbers.length <= 7) {
return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
} else {
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(
7
)}`;
}
};
// 인증번호 전송 함수
const sendVerification = () => {
if (phoneNumber.length < 10) return;
setVerificationStatus((prevStatus) => ({ ...prevStatus, sent: true }));
};
// 인증번호 재전송 함수
const resendVerification = () => {
setVerificationStatus((prevStatus) => ({
...prevStatus,
verified: false,
message: "",
}));
setVerificationCode("");
resetTimer();
};
// 인증번호 확인 함수
const verifyCode = () => {
if (verificationCode === "111111") {
setVerificationStatus({
sent: true,
verified: true,
message: "인증 성공",
});
} else {
setVerificationStatus({
sent: true,
verified: false,
message: "인증번호가 잘못되었습니다.",
});
}
};
// 타이머 함수
const formatTime = () => {
const minutes = Math.floor(timer / 60);
const seconds = timer % 60;
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
};
// 인증번호 입력 변경 함수
const handleVerificationCodeChange = (e: { target: { value: string } }) => {
const numbersOnly = e.target.value.replace(/\D/g, "");
if (numbersOnly.length <= VERIFICATION_CODE_LENGTH) {
setVerificationCode(numbersOnly);
}
};
useEffect(() => {
if (verificationStatus.sent) {
resetTimer();
}
}, [verificationStatus.sent]);
useEffect(() => {
if (verificationStatus.verified) {
stopTimer();
}
}, [verificationStatus.verified]);
return (
<Wrapper>
<Form.Wrapper>
<Form.Input
placeholder="01012345678"
type="text"
value={displayFormattedPhoneNumber(phoneNumber)}
onChange={handleChange}
/>
<Form.Button
onClick={
!verificationStatus.sent ? sendVerification : resendVerification
}
>
{!verificationStatus.sent ? "인증번호 전송" : "재전송"}
</Form.Button>
</Form.Wrapper>
{verificationStatus.sent && (
<Form.Wrapper>
<Form.Timer>{formatTime()}</Form.Timer>
<Form.Input
placeholder="인증번호 입력"
type="text"
value={verificationCode}
onChange={handleVerificationCodeChange}
/>
<Form.Button onClick={verifyCode}>확인</Form.Button>
{!verificationStatus.verified && (
<Form.Message color="red">
{verificationStatus.message}
</Form.Message> // 인증 실패 메시지
)}
{verificationStatus.verified && (
<Form.Message color="green">
{verificationStatus.message}
</Form.Message> // 인증 성공 메시지
)}
</Form.Wrapper>
)}
</Wrapper>
);
}
const Wrapper = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
const Form = {
Wrapper: styled.div`
position: relative;
display: flex;
gap: 8px;
`,
Input: styled.input`
padding: 12px 18px;
border: none;
border-radius: 8px;
background-color: #f5f5f5;
&::placeholder {
color: #bebebe;
}
`,
Button: styled.button`
background: #1273e4;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
width: 100px;
&:hover {
background-color: #105cb6;
}
&:active {
background-color: #0e4a9b;
transform: scale(0.98);
}
`,
Timer: styled.div`
position: absolute;
right: 120px;
top: 50%;
transform: translate(0, -50%);
color: #1273e4;
font-weight: bold;
`,
Message: styled.div`
color: ${(props) => props.color};
position: absolute;
left: 10px;
bottom: -30px;
`,
};
'Frontend > React' 카테고리의 다른 글
[React] react-responsive-carousel 내맘대로 커스텀해보자! (0) | 2024.04.27 |
---|---|
vscode jsx파일 quick fix import안되는 오류 해결 (0) | 2024.02.05 |
React에서 2개의 그래프를 동시에 이쁘게 띄워보자 (react chart js) (0) | 2024.02.02 |