참고로, 공부의 목적으로 작성하는 내용이다.
배우는 입장에서 회원가입 페이지를 만드므로, 많이 미숙한 부분이 많을 것이다.
실수가 있을 수 있으니 양해 바란다.
이제부터 회원가입 페이지를 만들어본다.
codecamp
수강중 구현하는 과제가 있었는데, 재미있어서 리마인드차 다시 만들어본다.
참고로 button
을 만들어서 제출하는 과제가 있었는데, 그 부분도 같이 해결차 디자인도 다시 만들어 본다.
디자인도 Nenumorphism
으로 만들었다.
Nenumorphism
은 잘 사용하지 않는다고 하지만, 그냥 예뻐서 만들었다.
Figma
는 금방 익힐것 같기는 한데, 더 많이 살펴봐야겠다.
무엇보다Design System
이 미친듯이 막힌다.
시간날때, 주말에 하루 날 잡아서 왠종일 만들어봐야겠다.
Nenumorphism
은 그림자와 빛으로 각 요소를 표현한다.
잡스횽이 있을때, 기존의Skeuomorphism
을 많이 밀었는데, 단점은 너무 사물처럼 만들어야 해서 현재 트렌드에 맞지 않는다.현재는 심플하며, 눈에 잘띄며, 공간감이 있으며, 의미론적인 디자인이 트렌드이다.
즉, 4박자가 딱 맞아 떨어져야 좋은 디자인이라 하드라..
Material Design 을 보자. 멋지다.그 대체재로
Neumorphism
이 떠올랐으나,Flat Design
이 등장한다.
Flat Design
이후 너무 단순하다는 이유로Neumorphism
과Flat Desing
의 장점을 합친
Material Design
이 탄생한다.
Material Design
을 살펴보면 Elevation 이 그림자를 통해 요소의depth
를 표현한다. 멋지다!!
Neumorphism
이 너무 멋지게 생겨 먹어서 이것저것 참고해서 만들어 보았다.
CSS
공부하면서 이것 저것 표현하는데 적절하다 생각이 들었다.
공부차 Figma
의 box-shadow
를 참고 하기는 했지만,
왠만해서는 직접 구현하려고 노력했다.
참고로 Neumorphism Generator 도 있었다.
굳이 직접 안만들어도 될것 같다.
만들어진 완성본은 아래와 같다.
회원가입 부분만 만들었으며, 나머지는 버튼에 대한 상태값을 나타내기 위해 옆에 두었다.
이제부터 회원가입 페이지를 만들어본다.
디자인을 먼저 살펴보자.
Input
및Button
Design 시 각 상태값은 다음과 같다.상태값:
default
focus
hover
active
참고로, Error 상태를 표현하는 Design 은 따로 만들지 않았다.
input
은 따로active
시 상태값을 주지 않았다.
focus
된이후 글을 쓰기에 굳이active
가 필요하다는 생각은 하지 않았다.
radio
,checkbox
는checked
및non-checked
만 표현한다.
나머지 상태값을 만들면 오히려 조잡해지는 느낌이라, 2가지 상태 및disabbled
상태만 만들었다.참고로
radio
버튼이 디자인상과 완벽히 똑같이는 만들어 지지 않아, 약간 다르다!!
figma
로 만들때도,box-shadow
의 한계가 느껴져,photoshop
하듯blur
주고 여러layer
를 겹쳐서 만들었는데,CSS
에서도 같은방법을 사용하기에는 무리가 있었다. ㅠㅠ그렇다고 이미지로 넣고 싶지는 않아 최대한 비슷하게 만들어 본다.
이렇게 각
input
과button
을 조합하여 회원가입 창을 만들었다.
이제 만들어진 회원가입을 구현해볼것이다.
html 틀은 다음과 같다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>SignUp</title>
<link rel="stylesheet" href="signup.css">
</head>
<body>
<div class="wrapper">
<div class="header">
<h2 class="header__title">
회원가입을<br/>진행해 주세요.
</h2>
</div>
<div class="body__container">
<div class="input__wrapper neu__input">
<input type="text" id="name" placeholder="이름을 입력해 주세요.">
<input type="email" id="email" placeholder="이메일을 입력해 주세요.">
<input type="password" id="pw" placeholder="패스워드를 입력해 주세요.">
<input type="password" id="ck-pw" placeholder="패스워드를 다시 입력해주세요.">
</div>
<div class="phone__wrapper neu__input">
<input type="text" name="firstNumber" id="firstNumber" maxlength="3">
<span>-</span>
<input type="text" name="secondNumber" id="secondNumber" maxlength="4">
<span>-</span>
<input type="text" name="thirdNumber" id="thirdNumber" maxlength="4">
</div>
<div class="auth__wrapper">
<div class="auth">
<span id="auth__number">000000</span>
<button class="btn" id="auth__number__btn" disabled>인증 번호</button>
</div>
<div class="auth">
<span id="auth__time">3:00</span>
<button class="btn" id="auth__check__btn" disabled>인증 하기</button>
</div>
</div>
<div class="gender__wrapper">
<div class="gender">
<div>
<input type="radio" name="gender" id="female">
<label for="female">여자</label>
</div>
<div>
<input type="radio" name="gender" id="male">
<label for="male">남자</label>
</div>
</div>
</div>
<div class="agreement__wrapper">
<div class="agreement">
<input type="checkbox" name="agreement" id="agreement">
<label for="agreement">
정보를 제공하는데 동의합니다.
</label>
</div>
</div>
<div class="signup__wrapper">
<div class="signup">
<button class="signup__btn btn" disabled>회원 가입</button>
</div>
</div>
</div>
</div>
<script src="signup.js"></script>
</body>
</html>
공부하면서 느낀것이, HTML 구조를 잘 작성해야 나머지도 술술 풀린다는 것이다.
일단 CSS 사용시 flex
를 사용할 예정으로 flex
사용을 염두에 두고 구조를 만들어 보았다.
각각의 정렬 기준이 되는 요소들은 전부 *__wrapper
로 group
화 시켰으며, *__wrapper
내부의 요소들역시 정렬되어야 하는 상황이면 group
화 시켰다.
*__wrapper
내부에 굳이 div.name
형식으로 한번더 group
화 시켰는데, 이는 추후 div.error
를 javascript
를 통해 밑에 추가하기 위해 group
화 시킨것이다.
그러면 *__wrapper
내부에 div.name
과 div.error
두개 가 생성되며, 이를 flex: display; flex-direction: column;
으로 해주면 밑으로 예쁘게 정렬된다.
참고로,
input
관련 요소에는 따로div.name
형식으로 묶지 않았다.
input
요소는background-color
,color
및placeholder
로error
상태를 표시할 예정이다.사실 이렇게 만들고 후회한다.
placeholder
로 표현하기에, 기존의value
값을 삭제해야만 했다.placeholder
로 만드는것은사용자 편의성
상 좋지 않은 방법인듯 싶다.
*잘못 작성시 재입력할 수 있기에value
는 그대로 있는것이 좋을것 같다.하지만 일단 만들었으므로 그대로 진행한다!!
밑은
CSS
코드이다.
큰 틀로 잡은 아래의class
별로 구현해본다.
.wrapper
.header
.body__container
.input__wrapper
.phone__wrapper
auth__wrapper
.gender__wrapper
agreement__wrapper
signup__wrapper
* {
box-sizing: border-box;
}
body {
font-family: "Inter", serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: #0083CC;
background-color: #EFF9FF;
}
h2 {
font-size: 24px;
font-weight: bold;
margin: 0;
}
button {
border: none;
outline: none;
}
/* wrapper */
.wrapper {
background: linear-gradient(
159.46deg, #EFF9FF 8.76%,
#E0F4FF 107.64%
);
border: 1px solid rgba(0, 163, 255, 0.2);
border-radius: 30px;
box-shadow:
inset 1px 1px 3px rgba(0, 98, 153, .2),
inset 4px 4px 3px rgba(255, 255, 255),
0px 4px 10px rgba(0, 163, 255, 0.1),
20px 20px 30px rgba(0, 98, 153, 0.3),
-50px -50px 50px rgba(255, 255, 255),
inset -2px -2px 2px rgba(0, 98, 153, 0.2);
padding: 30px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
/* neu__input */
.neu__input input {
width: 347px;
height: 50px;
color: #0083CC;
font-size: 16px;
border-width: bold;
padding: 15px 32px;
background-color: #EFF9FF;
border: none;
border-radius: 10px;
transition: all .5s ease;
box-shadow: inset 0px -1px 1px rgba(224, 244, 255, 0.8), inset 0px 2px 1px #EFF9FF, inset -1px -5px 2px #FFFFFF, inset 1px 4px 4px rgba(0, 33, 51, 0.3);
}
.neu__input input.error {
margin-top: 0;
background: #ff8080;
color: white;
}
.neu__input input.error:focus {
background: #ff4b4b;
color: white;
}
.neu__input input.error:hover {
background: #fa6363;
color: white;
}
.neu__input input.error::placeholder {
color: white;
}
.neu__input input::placeholder {
color: #8EB6CC;
font-weight: bold;
font-size: 16px;
opacity: .4;
}
.neu__input input:hover {
background: #E0F4FF;
box-shadow: inset 0px -1px 1px rgba(224, 244, 255, 0.8), inset 0px 1px 0px #EFF9FF, inset -1px -5px 2px #FFFFFF, inset 1px 4px 4px rgba(0, 33, 51, 0.3);
}
.neu__input input:focus {
background: #D0EEFF;
outline: none;
box-shadow: inset 0px -1px 1px rgba(224, 244, 255, 0.8), inset 0px 1px 0px #EFF9FF, inset -1px -5px 2px #FFFFFF, inset 1px 4px 4px rgba(0, 33, 51, 0.3);
}
.neu__input input:disabled {
background: #E0F4FF;
opacity: 0.5;
box-shadow: 0px 4px 4px rgba(255, 255, 255, 0.2), 0px -10px 10px #FFFFFF, 0px 4px 4px rgba(0, 131, 204, 0.4), inset 0px -1px 1px rgba(255, 255, 255, 0.25);
}
/* btn */
.btn {
color: #0083CC;
padding: 10px 32px;
font-weight: bold;
border-radius: 10px;
background-color: #EFF9FF;
box-shadow: 0px 2px 4px rgba(0, 33, 51, 0.1), 0px 4px 4px rgba(0, 131, 204, 0.3), -4px -4px 5px #FFFFFF, inset 0px -1px 1px rgba(255, 255, 255, 0.4), inset 1px 1px 10px rgba(0, 65, 102, 0.05);
transition: all .5s ease;
cursor: pointer;
}
.btn:focus {
background-color: #E0F4FF;
}
.btn:hover {
background-color: #EFF9FF;
box-shadow: 1px 1px 4px rgba(0, 33, 51, 0.1), 0px 2px 2px rgba(0, 131, 204, 0.2), -2px 2px 5px #FFFFFF, inset 0px -3px 3px rgba(255, 255, 255, 0.4), inset 0px 3px 2px rgba(0, 65, 102, 0.2);
}
.btn:active {
box-shadow: 1px 1px 4px rgba(0, 33, 51, 0.1), 0px 2px 2px rgba(0, 131, 204, 0.2), -2px 2px 5px #FFFFFF, inset 0px -3px 3px rgba(255, 255, 255, 0.4), inset 0px 3px 2px rgba(0, 65, 102, 0.2);
background-color: #D0EEFF;
}
.btn:disabled {
opacity: .5;
background-color: #E0F4FF;
color: #8EB6CC;
pointer-events: none;
}
/* header */
.header {
margin-bottom: 54px;
}
/* input__wrapper */
.input__wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.input__wrapper input {
margin-bottom: 32px;
}
/* phone__wrapper */
.phone__wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.phone__wrapper input {
width: 100px;
height: 40px;
}
/* auth__wrapper */
.auth__wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.auth {
display: flex;
align-items: center;
margin-bottom: 32px;
}
.auth button {
margin-left: 16px;
}
/* gender__wrapper */
.gender__wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32px;
}
.gender {
display: flex;
flex-direction: row;
justify-content: center;
}
.gender div {
display: flex;
align-items: center;
padding-left: 32px;
}
.gender div:first-child {
padding-left: 0;
}
.gender div label {
padding-left: 8px;
}
.gender div input {
appearance: none;
width: 30px;
height: 30px;
background-color:#EFF9FF;
border-radius: 15px;
box-shadow:
-4px -2px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .2),
inset -2px 2px 7px #FFFFFF,
-4px -4px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .2);
transition: all .5s ease;
}
.gender__wrapper input:hover {
box-shadow:
inset -4px -2px 5px #fff,
inset 3px 4px 4px rgba(0, 131, 204, .4),
-3px 2px 7px #FFFFFF,
-4px -4px 5px #fff
3px 4px 4px rgba(0, 131, 204, .4);
transition: all .5s ease;
}
.gender__wrapper input:checked {
box-shadow:
inset -4px -2px 5px #fff,
inset 3px 4px 4px rgba(0, 131, 204, .4),
-3px 2px 7px #FFFFFF,
-4px -4px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .4);
transition: all .5s ease;
background: #D0EEFF;
}
/* agreemnet__wrapper */
.agreement__wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 32px;
}
.agreement {
display: flex;
flex-direction: row;
align-items: center;
}
.agreement label {
padding-left: 16px;
}
.agreement input[type="checkbox"] {
width: 20px;
height: 20px;
appearance: none;
border-radius: 5px;
background-color:#EFF9FF;
box-shadow: 0px 2px 4px rgba(0, 33, 51, 0.1), 0px 4px 4px rgba(0, 131, 204, 0.3), -4px -4px 5px #FFFFFF, inset 0px -1px 1px rgba(255, 255, 255, 0.4), inset 1px 1px 10px rgba(0, 65, 102, 0.05);
transition: all .8s ease;
}
.agreement input[type="checkbox"]:checked {
background: #D0EEFF;
box-shadow: inset 0px 2px 4px rgba(0, 131, 204, 0.2), 0px 2px 2px rgba(0, 131, 204, 0.3), -4px -4px 10px #FFFFFF, inset 0px -1px 1px #fff, inset 2px 2px 10px rgba(0, 65, 102, 0.05), inset -2px -2px 4px #fff;
transition: all .5s ease;
}
.agreement input[type="checkbox"]:disabled {
background: #E0F4FF;
opacity: 0.5;
}
/* signup__wrapper */
.signup__wrapper {
display: flex;
justify-content: center;;
align-items: center;
}
.signup__wrapper .signup__btn {
width: 100%;
}
/* .error */
.error {
color: #ff4b4b;
margin-top: 8px;
}
각 코드를 분할해 본다.
자식
input
을neumorphism input
으로 만드는class
각input
에다class
지정이 싫어서, 부모에게 주도록styling
했다.
/* neu__input */
.neu__input input {
width: 347px;
height: 50px;
color: #0083CC;
font-size: 16px;
border-width: bold;
padding: 15px 32px;
background-color: #EFF9FF;
border: none;
border-radius: 10px;
transition: all .5s ease;
box-shadow: inset 0px -1px 1px rgba(224, 244, 255, 0.8), inset 0px 2px 1px #EFF9FF, inset -1px -5px 2px #FFFFFF, inset 1px 4px 4px rgba(0, 33, 51, 0.3);
}
.neu__input input::placeholder {
color: #8EB6CC;
font-weight: bold;
font-size: 16px;
opacity: .4;
}
.neu__input input:hover {
background: #E0F4FF;
box-shadow: inset 0px -1px 1px rgba(224, 244, 255, 0.8), inset 0px 1px 0px #EFF9FF, inset -1px -5px 2px #FFFFFF, inset 1px 4px 4px rgba(0, 33, 51, 0.3);
}
.neu__input input:focus {
background: #D0EEFF;
outline: none;
box-shadow: inset 0px -1px 1px rgba(224, 244, 255, 0.8), inset 0px 1px 0px #EFF9FF, inset -1px -5px 2px #FFFFFF, inset 1px 4px 4px rgba(0, 33, 51, 0.3);
}
.neu__input input:disabled {
background: #E0F4FF;
opacity: 0.5;
box-shadow: 0px 4px 4px rgba(255, 255, 255, 0.2), 0px -10px 10px #FFFFFF, 0px 4px 4px rgba(0, 131, 204, 0.4), inset 0px -1px 1px rgba(255, 255, 255, 0.25);
}
여기서 box-shadow
를 주었는데, box-shadow
속성은 아래와 같다
Default value | x-offset | y-offset | blur | spread | color |
---|---|---|---|---|---|
none optional | x축의 offset | y축의 offset | blur 값 optional | 그림자 확장 수치 optional | 색상 |
Default value 는 몇가지로 나누어진다.
name | description |
---|---|
inset | 내부에 그림자를 준다. |
initial | 외부에 그림자를 주는것이 기본값으로 되어 있다.initial 은 외부에 그림자를 준다.default |
inherit | 부모 element 로 부터 상속받는다. |
box-shadow
에 중복 적용하면, 여러 그림자를 겹칠 수 있다.
그러므로 조금더 매끄러운 그림자를 만들 수 있으며, 잘만 하면 튀어나온 것처럼 만들 수 있다.
.neu__input input {
/*...중략*/
box-shadow:
inset 0px -1px 1px rgba(224, 244, 255, 0.8),
inset 0px 2px 1px #EFF9FF,
inset -1px -5px 2px #FFFFFF,
inset 1px 4px 4px rgba(0, 33, 51, 0.3);
}
inset 0px -1px rgba(244, 244, 255, 0.8)
은 input
의 내부에 right bottom y축
으로 -1px
의 rgba
값을 준다는 것이다. 이때 rgba
의 마지막 값 0.8
은 opacity
이다. 또한 blur
를 1px
만 주었는데, 이는 blur
가 넓게 퍼지지 않도록 하는 효과이다.
그럼 input
의 오른쪽 하단에 1px
의 하얀색 투명한 라인이 만들어진다.
이는 해당 인풋의 외곽을 선명하기 위한 아래선이라고 생각하면된다.
inset -1px -5px 2px #FFFFFF
은 오른쪽 아래 y축
을 5px
만큼 준다.
5px
의 선이 생기며, 2px
의 blur
를 주어 선이 명확하지 않도록 흐릿하게 번지는 느낌을 주었다.
inset 0px 2px 1px #EFF9FF
은 #EFF9FF
의 색상을 왼쪽 상단의 y축
으로 2px
을 주고 blur
를 2px
만큼주어 약간의 흐릿한 경계를 준다.
어두운 색상은 경계가 명확하게 나타나기에, 1px
이 아닌 2px
로 주었다.
inset 1px 4px 4px rgba(0, 33, 51, 0.3)
은 어두운 생상을 넓게 blur
를 주기위해 추가한 값이다. 다른 부분과는 다르게 x축
으로 1px
을 주었는데, 이는 왼쪽부분에 어두운 부분이 더 나와서 입체감있게 만들기 위함이다.
이렇게 box-shadow
를 겹치고 겹쳐서 해당 neu__input
을 표현했다.
나머지, active
hover
focus
disabled
placeholder
역시 전부 이러한 방식으로 만든것이다.
.input
이 error
일때역시 표현하기 위해 배경색과 글자색을 지정한다.
.neu__input input.error {
background: #ff8080;
color: white;
}
.neu__input input.error:focus {
background: #ff4b4b;
color: white;
}
.neu__input input.error:hover {
background: #fa6363;
color: white;
}
.neu__input input.error::placeholder {
color: white;
}
.btn
class 를 가진button
을neumorphism
버튼으로 만드는class
이다.
/* btn */
.btn {
color: #0083CC;
padding: 10px 32px;
font-weight: bold;
border-radius: 10px;
background-color: #EFF9FF;
box-shadow: 0px 2px 4px rgba(0, 33, 51, 0.1), 0px 4px 4px rgba(0, 131, 204, 0.3), -4px -4px 5px #FFFFFF, inset 0px -1px 1px rgba(255, 255, 255, 0.4), inset 1px 1px 10px rgba(0, 65, 102, 0.05);
transition: all .5s ease;
cursor: pointer;
}
.btn:focus {
background-color: #E0F4FF;
}
.btn:hover {
background-color: #EFF9FF;
box-shadow: 1px 1px 4px rgba(0, 33, 51, 0.1), 0px 2px 2px rgba(0, 131, 204, 0.2), -2px 2px 5px #FFFFFF, inset 0px -3px 3px rgba(255, 255, 255, 0.4), inset 0px 3px 2px rgba(0, 65, 102, 0.2);
}
.btn:active {
box-shadow: 1px 1px 4px rgba(0, 33, 51, 0.1), 0px 2px 2px rgba(0, 131, 204, 0.2), -2px 2px 5px #FFFFFF, inset 0px -3px 3px rgba(255, 255, 255, 0.4), inset 0px 3px 2px rgba(0, 65, 102, 0.2);
background-color: #D0EEFF;
}
.btn:disabled {
opacity: .5;
background-color: #E0F4FF;
color: #8EB6CC;
pointer-events: none;
}
active
hover
focus
disabled
역시 구현한다.
전체
*__wrapper
를 둘러싼div
이다.
.wrapper {
background: linear-gradient(
159.46deg, #EFF9FF 8.76%,
#E0F4FF 107.64%
);
border: 1px solid rgba(0, 163, 255, 0.2);
border-radius: 30px;
box-shadow:
inset 1px 1px 3px rgba(0, 98, 153, .2),
inset 4px 4px 3px rgba(255, 255, 255),
0px 4px 10px rgba(0, 163, 255, 0.1),
20px 20px 30px rgba(0, 98, 153, 0.3),
-50px -50px 50px rgba(255, 255, 255),
inset -2px -2px 2px rgba(0, 98, 153, 0.2);
padding: 30px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.wrapper
에서 background
를 linear-gradient
를 사용해 주었으며, display: flex
를 사용하여 가운데 정렬에 사용한다.
flex-direction
을 flex-start
로 주었는데, .header
의 h2
태그를 왼쪽으로 정렬하기 위해서 이다.
이때 padding
값은 30px
로 내부컨텐츠를 둘러싼 공백을 균등하게 준다.
box-shadow:
inset 1px 1px 3px rgba(0, 98, 153, .2),
inset 4px 4px 3px rgba(255, 255, 255),
0px 4px 10px rgba(0, 163, 255, 0.1),
20px 20px 30px rgba(0, 98, 153, 0.3),
-50px -50px 50px rgba(255, 255, 255),
inset -2px -2px 2px rgba(0, 98, 153, 0.2);
box-shadow
는 기본적으로 initail
일때 right
, bottom
에 그림자가 만들어지며,
inset
일 경우 해당 요소내부의 left
, top
부분에 그림자가 만들어진다.
위의 box-shadow
에 -
값이 있는 px
을 보게될텐데, -px
은 그림자 생성위치의 반대로 그림자가 만들어진다
즉, initial
일때, left
, top
에 그림자가 만들어지며, inset
은 right
, bottom
에서 그림자가 생긴다.
이렇게 그림자를 여러개 겹치고 겹치면, 마치 위의 .wrapper
처럼 위로 튀어나온것 같은 효과를 만들 수 있다.
상단 부분의
header
의h2
를 둘러싼div
이다.
h2 {
font-size: 24px;
font-weight: bold;
margin: 0;
}
.header {
margin-bottom: 54px;
}
.header
내부의 h2
를 따로 지정한다.
여기서는 아니지만, Design System
을 만들어 각 font
값을 미리 지정하는것이 좋다.
font
는 다양하지 않고 딱 정해진 규칙대로 일관적인 Design
을 제공하는건 편집디자인
의 기본이다.
.header
의 하단에 여백을 주기 위해 margin-bottom: 54px;
을 준다.
.body__container
는 본 내용들을 다음 몸체div
이다.
여기서는 지정해 놓고 사용은 안했다.
괜히 만들었다.
name
password
check password
에 대한input
을 둘러싼div
이다.
/* input__wrapper */
.input__wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input__wrapper input {
margin-bottom: 32px;
}
.input__wrapper
에 flex
를 주고 flex-direction: column
으로 준다.
.input__wrapper input
들을 세로로 정렬시켜준다.
.input__wrapper
내부의 input
들은 서로간에 여백을 주기위해 margin-bottom
을 32px
로 준다.
핸드폰 인증할때 사용할
input
을 둘러싼div
이다.
/* phone__wrapper */
.phone__wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.phone__wrapper input {
width: 100px;
height: 40px;
}
.phone__wrapper
에 flex
를 주었으며 justify-content
를 space-between
을 준다.
.phone__wrapper input
들은 가로로 정렬되어 있으며, 양끝으로 정렬될 수 있도록 space-between
을 주는것이다.
.phone__wrapper input
은 고정값을 주기 위해 width
및 height
의 값을 넣어준다.
안 넣어주면 width
값이 넘쳐서 보기 좋지 못해지더라..
핸드폰 인증
button
을 가진div
이다.
.auth__wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.auth {
display: flex;
align-items: center;
margin-bottom: 32px;
}
.auth button {
margin-left: 16px;
}
.auth__wrapper
에는 flex-direction
는 column
을 준다.
주어진 div
를 세로로 정렬해야 한다.
성별 선택을 둘러싼
div
이다.
.gender__wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32px;
}
.gender {
display: flex;
flex-direction: row;
justify-content: center;
}
.gender div {
display: flex;
align-items: center;
padding-left: 32px;
}
.gender div:first-child {
padding-left: 0;
}
.gender div label {
padding-left: 8px;
}
.gender div input {
appearance: none;
width: 30px;
height: 30px;
background-color:#EFF9FF;
border-radius: 15px;
box-shadow:
-4px -2px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .2),
inset -2px 2px 7px #FFFFFF,
-4px -4px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .2);
transition: all .5s ease;
}
.gender__wrapper input:hover {
box-shadow:
inset -4px -2px 5px #fff,
inset 3px 4px 4px rgba(0, 131, 204, .4),
-3px 2px 7px #FFFFFF,
-4px -4px 5px #fff
3px 4px 4px rgba(0, 131, 204, .4);
}
.gender__wrapper input:checked {
box-shadow:
inset -4px -2px 5px #fff,
inset 3px 4px 4px rgba(0, 131, 204, .4),
-3px 2px 7px #FFFFFF,
-4px -4px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .4);
transition: all .5s ease;
background: #D0EEFF;
}
기존의 *__wrapper
들 보다는 복잡해 보인다.
이는 radio input
을 새롭게 작성해야 하기에 추가적인 코드가 생성된다.
나중에 보겠지만,
input
관련해서css
를.btn
으로 따로 작성했다.
그래서 다른input
들은 따로 스타일을 작성하지 않도록 만들었다.
/* .gender 는 .gender__wrapper 내부의 div 이다. */
.gender div input {
appearance: none;
width: 30px;
height: 30px;
background-color:#EFF9FF;
border-radius: 15px;
box-shadow:
-4px -2px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .2),
inset -2px 2px 7px #FFFFFF,
-4px -4px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .2);
transition: all .5s ease;
}
여기서 중요한건 apparance
를 none
으로 두어 기본 default
로 설정된 radio
를 초기화 시킨다.
그로 인해 hover
checked
되었을때 스타일을 다시 작성해야 한다.
hover
checked
시에 변경될 스타일을 따로 각각 지정한다.
.gender__wrapper input:hover {
box-shadow:
inset -4px -2px 5px #fff,
inset 3px 4px 4px rgba(0, 131, 204, .4),
-3px 2px 7px #FFFFFF,
-4px -4px 5px #fff
3px 4px 4px rgba(0, 131, 204, .4);
}
.gender__wrapper input:checked {
box-shadow:
inset -4px -2px 5px #fff,
inset 3px 4px 4px rgba(0, 131, 204, .4),
-3px 2px 7px #FFFFFF,
-4px -4px 5px #fff,
3px 4px 4px rgba(0, 131, 204, .4);
background: #D0EEFF;
}
지정했을때, 위와 같다.
여기서 hover
시 box-shadow
를 재지정하여, 밑으로 움푹 들어간 듯한 이미지로 변경하고,
checked
시 에는 background
색상을 #D0EEFF
으로 변경시켜, 클릭시 색을 변경시킨다.
checkbox 선택을 둘러싼
div
이다.
.agreement__wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 32px;
}
.agreement {
display: flex;
flex-direction: row;
align-items: center;
}
.agreement label {
padding-left: 16px;
}
.agreement input[type="checkbox"] {
width: 20px;
height: 20px;
appearance: none;
border-radius: 5px;
background-color:#EFF9FF;
box-shadow: 0px 2px 4px rgba(0, 33, 51, 0.1), 0px 4px 4px rgba(0, 131, 204, 0.3), -4px -4px 5px #FFFFFF, inset 0px -1px 1px rgba(255, 255, 255, 0.4), inset 1px 1px 10px rgba(0, 65, 102, 0.05);
transition: all .8s ease;
}
.agreement input[type="checkbox"]:checked {
background: #D0EEFF;
box-shadow: inset 0px 2px 4px rgba(0, 131, 204, 0.2), 0px 2px 2px rgba(0, 131, 204, 0.3), -4px -4px 10px #FFFFFF, inset 0px -1px 1px #fff, inset 2px 2px 10px rgba(0, 65, 102, 0.05), inset -2px -2px 4px #fff;
transition: all .5s ease;
}
.agreement input[type="checkbox"]:disabled {
background: #171718;
opacity: 0.5;
}
input[type="checkbox"]
로 선택하여 해당 input
을 다시 수정했다.
또한 .agreement__wrapper
는 flex-direction
을 column
으로 주었는데,
이는 나중에 javascript
로 div.error
를 .agreement__wrapper
의 자식으로 넣어줄것이다. 그때, 세로로 정렬되어야 하므로 column
으로 준것이다.
회원가입 버튼을 둘러싼
div
이다.
.signup__wrapper {
display: flex;
justify-content: center;;
align-items: center;
}
.signup__wrapper .signup__btn {
width: 100%;
}
signup__wrapper .signup__btn
에 width: 100%
를 주었는데, 이는 해당 button
이 .wrapper
너비 만큼 차지 하기 위해 준것이다.
error
되었을때 표현하기 위한class
이다.
/* .error */
.error {
color: #ff4b4b;
}
div.error {
margin-top: 8px;
}
내가 만든 회원가입 페이지는 input.error
와 div.error
로 나뉘기에, 여기서 div.error
에 margin-top: 8px
을 준다.
Javascript
의 조건은 다음과 같다.
핸드폰번호 조건
. 핸드폰번호 입력시 첫번째 자리는 3개의 값을 입력한다.
. 두번째 자리, 세번째 자리는 4개의 값을 입력한다.
. 각 번호 입력이후 값의 갯수가 충족되면, 다음 자리수로 넘어가며, 마지막 세번째 자리는 갯수충족시 인증번호 버튼이 활성화 된다.
. 만약, 각 입력갯수에 중족하지 않는다면, 인증번호 버튼은 활성화 되지 않는다.
인증 조건
. 회원가입 버튼은 인증되기전까지 비활성화 된다.
. 핸드폰 번호를 입력이 완벽하게 된 이후 인증번호 버튼이 활성화 된다.
. 인증번호 버튼이 활성화 된 후 누르게 되면 랜덤한 6자리의 숫자를 나타낸다.
. 랜덤한 6자리 숫자가 나타나면, 인증번호 버튼은 비활성화 되며 인증완료 버튼이 활성화 된다.
. 인증완료 버튼이 활성화 됨과 동시에, 3:00분 카운트가 시작된다.
. 카운트가 0:00 이 되면 인증완료 버튼은 비활성화 되며, 인증번호 버튼이 활성화 되면서 6자리 숫자가 "000000" 으로 초기화된다.
. 인증완료 버튼을 누르면, 랜덤한 숫자 및 3:00 분 카운트 글자가 인증완료로 변경된다.
. 인증완료 되면 인증완료버튼, 인증번호버튼, 핸드폰번호 입력부분이 비활성화 되며 회원가입 버튼이 활성화 된다.
Error 조건
해당부분에 대해,
REGEXP
를 공부하면, 검증관련해서 추가할 생각이다.
일단 단순한input
검증을 한다.. 회원가입 버튼을 눌렀을때, 각 값에 대해 확인후, 틀리면
error
를 발생한다.
. 모든input
은 값이 있어야 한다. 비어있을 경우error
가 발생한다.
.email input
은 내용상@
가 있어야 한다. 내용이 없을경우error
가 발생한다.
.password check input
같은 경우password input
과 값이 동일해야 한다. 동일하지 않을 경우error
가 발생한다.
.password check input
같은 경우password input
과 값이 동일해야 한다. 동일하지 않을 경우error
가 발생한다.
. 성별 체크가 되지 않으면,error
가 발생한다.
. 동의란에 체크가 되지 않으며,error
가 발생한다.
Javascript
의 내용은 다음과 같다.
// phone__wrapper inputs
const $firstNumber = document.querySelector("#firstNumber");
const $secondNumber = document.querySelector("#secondNumber");
const $thirdNumber = document.querySelector("#thirdNumber");
// auth
const $authNumberBtn = document.querySelector("#auth__number__btn");
// helper
const random = (max) => Math.floor((Math.random() * max) + 1);
const passwordChecker = () => {
const pw = document.querySelector("#pw");
const ckPw = document.querySelector("#ck-pw");
return pw.value === ckPw.value;
};
const setError = (elem, desc, val) => {
elem.classList.add("error");
elem.placeholder = desc;
if (val || val === 0 || val === "") { elem.value = val }
return false;
}
const setDisabled = (elem, val=true) => {
if (typeof val === "boolean") {
elem.disabled = val; // elem 인자에 있는 요소를 disabled 한다.
// val 값을 통해 값 변경이 가능하며, 기본값은 true 이다.
} else {
console.error("boolean 타입이어야 합니다!!");
}
}
const phoneNumberChecker = () => {
const firstLen = document.querySelector("#firstNumber").value.length === 3;
const secondLen = document.querySelector("#secondNumber").value.length === 4;
const thirdLen = document.querySelector("#thirdNumber").value.length === 4;
return firstLen && secondLen && thirdLen;
};
// set focus func
const setFocus = (curElem, nextElem) => {
const $authNumberBtn = document.querySelector("#auth__number__btn");
curElem.addEventListener("input", (e) => {
let target = e.target;
let len = target.value.length;
if (target.id === "firstNumber") {
if (len >= 3) {
nextElem.focus();
}
} else if (target.id === "secondNumber") {
if (len >= 4) {
nextElem.focus();
} else if (len === 0) {
$firstNumber.focus();
}
} else if ( target.id === "thirdNumber") {
if (len === 0) {
$secondNumber.focus();
}
}
if (phoneNumberChecker()) {
setDisabled($authNumberBtn, false);
} else {
setDisabled($authNumberBtn);
}
});
};
// set auth
const setAuth = (max, time) => {
const $authNumberBtn = document.getElementById("auth__number__btn");
const $authCheckBtn = document.querySelector("#auth__check__btn");
const $signupBtn = document.querySelector(".signup__btn");
$authNumberBtn.addEventListener("click", () => {
let t = time;
$authNumberBtn.disabled = true;
$authNumberBtn.previousElementSibling.textContent = String(random(max)).padStart(6, "0");
let started = false;
let min = 0;
let sec = 0;
const interId = setInterval(() => {
if (!started) {
if (t >= 0 ) {
min = Math.floor(t / 60);
sec = String(Math.floor(t % 60)).padStart(2, "0");
$authCheckBtn.previousElementSibling.textContent = `${min}:${sec}`
$authCheckBtn.disabled = false;
t -= 1;
} else {
started = true;
}
} else {
clearInterval(interId);
setDisabled($authCheckBtn);
setDisabled($authNumberBtn, false);
$authCheckBtn.previousElementSibling.textContent = `3:00`;
$authNumberBtn.previousElementSibling.textContent = "000000";
}
}, 1000);
$authCheckBtn.addEventListener("click", () => {
alert("인증되었습니다.");
const phoneInputs = document.querySelectorAll(".phone__wrapper input");
clearInterval(interId);
$authCheckBtn.previousElementSibling.textContent = `인증완료`;
$authNumberBtn.previousElementSibling.textContent = "인증완료";
setDisabled($authCheckBtn);
setDisabled($authNumberBtn);
setDisabled($signupBtn, false);
phoneInputs.forEach(input => {
setDisabled(input)
});
});
});
};
// input error check
const checkInputs = () => {
const inputs = document.querySelectorAll(".input__wrapper input");
let checker = true;
inputs.forEach(input => {
const id = input.id;
const len = input.value.length;
if (id == "name" && len === 0) {
checker = setError(input, "이름이 입력되지 않았습니다.");
} else if (id == "email") {
if (len === 0) {
checker = setError(input, "이메일이 입력되지 않았습니다.");
} else if (!input.value.includes("@")) {
checker = setError(input, "잘못된 이메일 형식입니다." , "");
}
} else if (id === "pw" && len === 0) {
checker = setError(input, "비밀번호가 입력되지 않았습니다." );
} else if (id === "ck-pw") {
if (len === 0) {
checker = setError(input, "비밀번호가 입력되지 않았습니다." );
} else if (!passwordChecker()) {
checker = setError(input, "비밀번호가 일치하지 않았습니다.", "");
}
}
});
return checker;
};
// radios checker
const checkGender = () => {
const radios = document.getElementsByName("gender");
let checker = false;
radios.forEach(radio => {
if (checker) {
return;
}
checker = radio.checked
});
if (!checker) {
radios[0]
.parentElement
.parentElement
.insertAdjacentHTML(
"afterend", `
<div class="error">둘중 하나를 선택해 주세요.</div>
`);
}
return checker;
};
const checkAgreement = () => {
const agreement = document.querySelector("#agreement");
const checked = agreement.checked;
if (!checked) {
agreement
.parentElement
.insertAdjacentHTML(
"afterend", `
<div class="error">동의해 주셔야 회원가입이 가능합니다.</div>
`);
return checked;
} else {
return checked;
}
}
// removeErrorClass
const removeError = () => {
const inputErrors = document.querySelectorAll("input.error");
const divErrors = document.querySelectorAll("div.error");
inputErrors.forEach(error => {
error.classList.remove("error");
});
divErrors.forEach(error => {
error.remove();
});
};
const resetSignup = () => {
const $phoneInputs = document.querySelectorAll(".phone__wrapper input");
const $bodyInputs = document.querySelectorAll(".input__wrapper input");
const $radios = document.getElementsByName("gender");
const $checkbox = document.querySelector("#agreement");
const $authNumberBtn = document.querySelector("#auth__number__btn");
const $authCheckBtn = document.querySelector("#auth__check__btn");
const $signupBtn = document.querySelector(".signup__btn");
$phoneInputs.forEach(input => {
input.disabled = false;
input.value = "";
});
$bodyInputs.forEach(input => {
input.disabled = false;
input.value = "";
});
$radios.forEach(radio => radio.checked = false);
$checkbox.checked = false;
$authNumberBtn.previousElementSibling.textContent = "000000";
$authCheckBtn.previousElementSibling.textContent = "3:00";
setDisabled($authNumberBtn);
setDisabled($authCheckBtn);
setDisabled($signupBtn);
}
// signup
const signup = () => {
const $signupBtn = document.querySelector(".signup__btn");
$signupBtn.addEventListener("click", () => {
removeError();
const checkArr = [checkInputs(), checkGender(), checkAgreement()];
for ( let bool of checkArr) {
console.log(bool);
}
if (checkArr[0] && checkArr[1] && checkArr[2]) {
resetSignup();
alert("가입을 축하합니다.");
} else {
console.error("가입안됨");
}
});
}
setFocus($firstNumber, $secondNumber);
setFocus($secondNumber, $thirdNumber);
setFocus($thirdNumber, $authNumberBtn);
setAuth(999999, 121);
signup();
위 내용을 하나씩 리마인드 해본다.
핸드폰번호 입력 조건
. 핸드폰번호 입력시 첫번째 자리는 3개의 값을 입력한다.
. 두번째 자리, 세번째 자리는 4개의 값을 입력한다.
. 각 번호 입력이후 값의 갯수가 충족되면, 다음 자리수로 넘어가며, 마지막 세번째 자리는 갯수충족시 인증번호 버튼이 활성화 된다.
. 만약, 각 입력갯수에 중족하지 않는다면, 인증번호 버튼은 활성화 되지 않는다.
setFocus
함수를 사용하여 핸드폰 번호 입력시, 해당 갯수가 충족되면 다음으로 넘어가는 함수를 구현해 보았다.
// set focus func
const setFocus = (curElem, nextElem) => {
const $authNumberBtn = document.querySelector("#auth__number__btn");
// 인증 번호 버튼 요소를 가져온다.
curElem.addEventListener("input", (e) => {
let target = e.target; // 선택한 요소
let len = target.value.length; // 선택한 요소의 길이
if (target.id === "firstNumber") {
if (len >= 3) { // 선택한 요소의 id 가 "firstNumber" 이고
// len 값이 3이 되면
nextElem.focus(); // 다음 Element 로 이동
}
} else if (target.id === "secondNumber") {
if (len >= 4) { // 선택한 요소의 id 가 "secondNumber" 이고
// len 값이 4이 되면
nextElem.focus(); // 다음 Element 로 이동
} else if (len === 0) {
$firstNumber.focus();
}
} else if ( target.id === "thirdNumber") {
if (len === 0) { // 선택한 요소의 id 가 "thirdNumber" 이고
// len 값이 0이면 이전 요소로 이동
$secondNumber.focus();
}
}
if (phoneNumberChecker()) { // phoneNumberChecker 를 호출하여
// input.value 의 length 값을 확인한다.
// input.value.length 가 값이 조건에 맞는다면,
// true 를 아니면 false 를 반환한다.
setDisabled($authNumberBtn, false); // setDisabled 를 호출하여
// 인증번호 버튼의 disabled 를
// 해제한다.
} else {
setDisabled($authNumberBtn); // phoneNumberChecker 가 false 라면
// 인증번호 버튼을 disabled 한다.
}
});
};
위에는 편의상 코드가 너무 길어져, helper function
이라고 지은 함수를 호출한다.
이렇게 코드를 분리하면, 코드기능을 더 알아보기가 쉬워진다.
// helper
const phoneNumberChecker = () => {
// 각 input 의 length 값을 가져와 변수에 담는다.
const firstLen = document.querySelector("#firstNumber").value.length === 3;
const secondLen = document.querySelector("#secondNumber").value.length === 4;
const thirdLen = document.querySelector("#thirdNumber").value.length === 4;
// 담은 변수를 && 조건 연산자를 통해, 한개라도 false 가 있다면 false 를 반환한다.
// 만약 전부다 true 일경우에는 true 를 반환한다.
return firstLen && secondLen && thirdLen;
};
const setDisabled = (elem, val=true) => {
if (typeof val === "boolean") { // val 이 boolean 타입인지 확인한다.
elem.disabled = val; // elem 인자에 있는 요소를 disabled 한다.
// val 값을 통해 값 변경이 가능하며, 기본값은 true 이다.
// true 를 주면 요소는 disabled 된다.
} else {
console.error("boolean 타입이어야 합니다!!");
}
}
인증 조건
. 회원가입 버튼은 인증되기전까지 비활성화 된다.
. 핸드폰 번호를 입력이 완벽하게 된 이후 인증번호 버튼이 활성화 된다.
. 인증번호 버튼이 활성화 된 후 누르게 되면 랜덤한 6자리의 숫자를 나타낸다.
. 랜덤한 6자리 숫자가 나타나면, 인증번호 버튼은 비활성화 되며 인증완료 버튼이 활성화 된다.
. 인증완료 버튼이 활성화 됨과 동시에, 3:00분 카운트가 시작된다.
. 카운트가 0:00 이 되면 인증완료 버튼은 비활성화 되며, 인증번호 버튼이 활성화 되면서 6자리 숫자가 "000000" 으로 초기화된다.
. 인증완료 버튼을 누르면, 랜덤한 숫자 및 3:00 분 카운트 글자가 인증완료로 변경된다.
. 인증완료 되면 인증완료버튼, 인증번호버튼, 핸드폰번호 입력부분이 비활성화 되며 회원가입 버튼이 활성화 된다.
setAuth
함수의 코드는 이렇다.
// set auth
const setAuth = (max, time) => {
const $authNumberBtn = document.getElementById("auth__number__btn");
// 인증 번호 버튼을 가져온다.
const $authCheckBtn = document.querySelector("#auth__check__btn");
// 인증 체크 버튼을 가져온다.
const $signupBtn = document.querySelector(".signup__btn");
// 회원가입 버튼을 가져온다. 회원가입 버튼은 인증되면 활성화한다.
$authNumberBtn.addEventListener("click", () => {
// 인증 번호 버튼에 클릭 이벤트를 할당한다.
let t = time; // time 인자의 값을 t 변수에 할당한다.
// time 인자를 복사없이 사용하면
// 나중에 closure 로 인해 time 값 자체가
// 변하게 되므로, 기존 time 값을 유지하기 위해 할당하는 것이다.
setDisabled($authNumberBtn);
// 인증 번호 버튼을 눌렀기에, 인증번호 버튼을 비활성화 한다.
$authNumberBtn.previousElementSibling.textContent = String(random(max)).padStart(6, "0"); // 이때 인증번호 버튼의 형제인 span 에
// random 함수를 불러와 랜덤한 숫자값을
// 가져온다.
// 만들어진 숫자를 문자열로 만든이후,
// padStart 를 통해 숫자값이 6자리보다
// 적다면, "0" 을 채워 6자리로 만든다.
let started = false; // started 는 setInterval 이 실행되었었는지
// 아닌지 확인하는 변수이다.
// 초기값은 시작 안되었으니 false 이다.
let min = 0; // min 값은 아직 설정하지 않아 0 으로 초기화한다.
let sec = 0; // sec 도 마찬가지이다. setInterval 시 값을 할당한다.
const interId = setInterval(() => {
if (!started) { // started 값이 false 이므로, not 연산자로 확인한다.
if (t >= 0 ) { // t 값이 0 보다 작으면 if 몸체는 실행되지 않는다.
min = Math.floor(t / 60);
// min 값을 구한다.
// Math.floor 를 사용하여, 소수점 내림한다.
sec = String(Math.floor(t % 60)).padStart(2, "0");
// sec 값을 구한다. sec 값에서 소수점 내림한후, padStart를 통해
// 2자리수가 될 수 있도록 만든다.
$authCheckBtn.previousElementSibling.textContent = `${min}:${sec}`
// 만들어진 min 과 sec 을 template operator 를 사용하여
// 변수를 가져와 대입한다.
$authCheckBtn.disabled = false;
// 인증확인 버튼을 활성화 시킨다.
t -= 1; // interval 이 실행될때마다, t 값에 -1 하여
// count 를 감소시킨다.
} else {
started = true; // 만약 t 값이 0 보다 작으면 started 를
// true 로 한다.
// 그럼 다음 interval 때, 밑의 코드로 넘어가
// clearInterval 을 통해 interval 을
// 중지시킨다.
}
} else {
// started 가 true 일때
// started 가 true 인것은 count 가 0 이 되었다는 것이다.
clearInterval(interId); // interval 을 중지시킨다.
// 이때, interId 를 가져오는데,
// interId 는 closure 를 통해 가져오므로
// 변수참조가 가능하다.
setDisabled($authCheckBtn); // 인증확인 버튼을 disabled 시킨다.
setDisabled($authNumberBtn, false);
// 인증번호 버튼을 활성화 시킨다.
$authCheckBtn.previousElementSibling.textContent = `3:00`;
// 인증확인 버튼의 카운트 시간을 "3:00" 으로 초기화 시킨다.
$authNumberBtn.previousElementSibling.textContent = "000000";
// 인증번호 버튼의 6자리 숫자를 "000000" 으로 초기화 시킨다.
}
}, 1000); // interval 은 1초마다, 등록된 함수를 매번 실행시킨다.
$authCheckBtn.addEventListener("click", () => {
// 인증확인 버튼이 활성화 될때만, 클릭이 가능하다.
// 그러므로 interval 시에만 click 가능하게 만들 필요가 없다고 생각했다.
// setAuth 함수가 호출될때, 따로 인증확인 버튼에 click 이벤트를 할당한다.
alert("인증되었습니다.");
// 인증확인 버튼을 누르면 해당 alert 창이 나온다.
const phoneInputs = document.querySelectorAll(".phone__wrapper input");
// .phone__wrapper 안의 input 요소를 NodeList 로 가져온다.
clearInterval(interId);
// 인증확인 버튼 클릭시 interval 을 중단시킨다.
$authCheckBtn.previousElementSibling.textContent = `인증완료`;
// 인증확인 버튼의 카운트 시간을 "인증완료" 문구로 변경한다.
$authNumberBtn.previousElementSibling.textContent = "인증완료";
// 인증번호 버튼의 6자리 숫자를 "인증완료" 문구로 변경한다.
setDisabled($authCheckBtn);
// 인증확인 버튼을 disabled 한다.
setDisabled($authNumberBtn);
// 인증번호 버튼을 disabled 한다.
setDisabled($signupBtn, false);
// 회원가입 버튼을 활성화한다.
phoneInputs.forEach(input => {
setDisabled(input)
// .phone__wrapper input 들을 disabled 한다.
});
});
});
};
사용된 helper
함수는 다음과 같다.
const setDisabled = (elem, val=true) => {
if (typeof val === "boolean") { // booelan 타입인지 check 한다.
elem.disabled = val; // elem 인자에 있는 요소를 disabled 한다.
// val 값을 통해 값 변경이 가능하며, 기본값은 true 이다.
} else {
console.error("boolean 타입이어야 합니다!!");
}
} // 앞전에 설명한 disabled 시키는 함수이다.
const phoneNumberChecker = () => {
// phoneNumber 의 length 값을 확인하여, boolean 값으로 반환하는 함수이다.
const firstLen = document.querySelector("#firstNumber").value.length === 3;
// 첫번째 input 요소의 length 를 할당한다.
const secondLen = document.querySelector("#secondNumber").value.length === 4;
// 두번째 input 요소의 length 를 할당한다.
const thirdLen = document.querySelector("#thirdNumber").value.length === 4;
// 세번째 input 요소의 length 를 할당한다.
return firstLen && secondLen && thirdLen;
// && operator 를 사용하여, 한개의 값이라도 false 이면 false 를 반환하며,
// 그렇지 않을 경우 true 를 반환한다.
}
Error 조건
해당부분에 대해,
REGEXP
를 공부하면, 검증관련해서 추가할 생각이다.
일단 단순한input
검증을 한다.. 회원가입 버튼을 눌렀을때, 각 값에 대해 확인후, 틀리면
error
를 발생한다.
. 모든input
은 값이 있어야 한다. 비어있을 경우error
가 발생한다.
.email input
은 내용상@
가 있어야 한다. 내용이 없을경우error
가 발생한다.
.password check input
같은 경우password input
과 값이 동일해야 한다. 동일하지 않을 경우error
가 발생한다.
. 성별 체크가 되지 않으면,error
가 발생한다.
. 동의란에 체크가 되지 않으며,error
가 발생한다.
error
함수를 만드는데 총 3가지의 함수로 만들었다.
input
을 체크하는checkInputs
과 성별을 체크하는 checkGender
,
동의란을 체크하는 checkAgreement
이다.
그런후 signup
함수를 호출하여, 해당 함수들이 전부 ture
를 반환할때,
가입되었습니다
라는 alret
이 나오도록 만들었다.
각 함수를 확인해 보자.
checkInput 구현 조건
. 모든input
은 값이 있어야 한다. 비어있을 경우error
가 발생한다.
.email input
은 내용상@
가 있어야 한다. 내용이 없을경우error
가 발생한다.
.password check input
같은 경우password input
과 값이 동일해야 한다. 동일하지 않을 경우error
가 발생한다.
helper
함수먼저 보도록 한다.
const setError = (elem, desc, val) => {
// elem 은 Element 를 인자로 받는다.
// desc 는 placeholder 에 넣을 문자열을 받는다.
// val 값은 elem 의 value 값을 할당한다.
elem.classList.add("error");
// 요소의 클래스에 .error 를 추가한다.
// 그럼 요소는 .error 클래스로 인해
// 시각적으로 error 임이 표시된다.
elem.placeholder = desc;
// 요소의 placeholder 에 넣을 문자열을 할당한다.
// error 표시는 placeholder 를 통해 표현해보고자 구현했지만,
// 지금은 그렇게 좋은 방법은 아닌듯 싶다.
if (val || val === 0 || val === "") { elem.value = val }
// val 값을 넣는데,
// 자바스크립트는 0, "" 값을 undefined 로 인식하므로 or operator 를 통해
// 0, "" 이더라도 요소의 value 값을 할당할 수 있도록 한다.
// 만약, 값이 정말 undefined 나 null 이라면 해당 if 문으로 인해 if 몸체
// 코드는 작동하지 않는다.
return false;
// error 를 설정했으므로, checker 에서 사용될 값 false 를 반환한다.
}
const passwordChecker = () => {
// password 가 동일한지 확인하는 함수
const pw = document.querySelector("#pw");
// #pw 의 요소를 가져온다
const ckPw = document.querySelector("#ck-pw");
// #ck-pw 의 요소를 가져온다.
return pw.value === ckPw.value;
// 가져온 요소의 value 값을 서로 비교한다.
// 값이 같으면 true를 아니면 false 반환.
};
위의 helper
함수를 아래의 checkInputs
에서 사용했다.
// input error check
const checkInputs = () => {
const inputs = document.querySelectorAll(".input__wrapper input");
// .input__wrapper 의 모든 input 을 가져온다.
let checker = true;
// checker 는 일단 true 로 설정한후
// error 가 발생하면 false 로 변경되는 변수이다.
// 이 변수의 상태값을 반환하여, 이 함수에 error 가 있는지 확인한다.
// 만약 false 라면, input 들 어딘가에 문제가 있는것이고,
// true 라면 아무 문제 없다는 뜻이다.
inputs.forEach(input => {
// 각 input 을 가져온다.
const id = input.id;
// input 의 id 를 가져온다.
const len = input.value.length;
// input 의 length 값을 가져온다.
if (id == "name" && len === 0) {
// 요소의 id 가 name 이고 value 의 length 값이 0 이라면,
// setError 함수를 실행시키고, 반환값은 false 가 반환된다.
// 즉 checker 는 false 가 된다.
checker = setError(input, "이름이 입력되지 않았습니다.");
} else if (id == "email") {
// 요소의 id 가 email 이면, 아래의 코드를 실행시킨다.
if (len === 0) {
// 해당 요소의 value 의 length 가 0 이면
// setError 함수가 호출되고 checker 에 false 값을 할당한다.
checker = setError(input, "이메일이 입력되지 않았습니다.");
} else if (!input.value.includes("@")) {
// 요소 value 에 "@" 이 포함되지 않았다면,
// setError 함수가 호출되고 checker 에 false 값을 할당한다.
checker = setError(input, "잘못된 이메일 형식입니다." , "");
}
} else if (id === "pw" && len === 0) {
// 요소의 id 가 pw 이고 value 의 length 값이 0 이라면,
// setError 함수를 실행시키고, 반환값은 false 가 반환된다.
// 즉 checker 는 false 가 된다.
checker = setError(input, "비밀번호가 입력되지 않았습니다." );
} else if (id === "ck-pw") {
// 요소의 id 가 ck-pw 이면, 아래의 코드를 실행시킨다.
if (len === 0) {
// 해당 요소의 value 의 length 가 0 이면
// setError 함수가 호출되고 checker 에 false 값을 할당한다.
checker = setError(input, "비밀번호가 입력되지 않았습니다." );
} else if (!passwordChecker()) {
// password 재확인 input 이므로,
// id 가 pw 인 요소의 값과 비교하는 함수이다.
// 비교시 값이 일치하다면, true 를 아니면 false 를 반환한다.
// false 이면 setError 함수를 실행시키고,
// 반환값은 false 가 반환된다.
// 즉 checker 는 false 가 된다.
checker = setError(input, "비밀번호가 일치하지 않았습니다.", "");
}
}
});
return checker; // Error 체크이후, checker 를 반환한다.
};
이렇게 하면 checkInput
함수의 구현이 끝난다.
checkGender
함수는 성별을 구분하는radio input
이다.
checkGender 구현 조건
. 모든input
은 값이 있어야 한다. 비어있을 경우error
가 발생한다.
. 성별 체크가 되지 않으면,error
가 발생한다.
// radios checker
const checkGender = () => {
const radios = document.getElementsByName("gender");
// radio input 을 가져오기 위해,
// gender name 을 가진 radio 들을 가져온다.
let checker = false;
// checker 를 false 로 초기화 시킨다.
// Error 가 없다면 checker 는 true,
// Error 가 있다면 false 가 할당된다.
radios.forEach(radio => {
if (checker) {
// checker 가 true 면
// radio 버튼들 중 하나가 선택되었다는것이다.
// 그러므로 더이상 해당 값을 할당 필요없으므로
// 함수를 종료시킨다.
return;
}
checker = radio.checked
// radio input 의 checked 값을 가져온다.
// checked 값은 check 되었다면 true,
// 아니면 false 이다.
});
if (!checker) {
// 만약 checker 가 false 라면,
// radios 의 부모의 부모의 끝에
// <div class="error">둘중 하나를 선택해 주세요.</div>
// 요소를 추가한다.
radios[0]
.parentElement // radio 를 감싼 div
.parentElement // div.gender__wrapper
.insertAdjacentHTML(
"afterend", `
<div class="error">둘중 하나를 선택해 주세요.</div>
`);
}
return checker;
// checker 값 반환.
};
checkAgreement
함수는checkbox input
을 확인하는 함수이다.
checkGender 구현 조건
. 모든input
은 값이 있어야 한다. 비어있을 경우error
가 발생한다.
. 동의란에 체크가 되지 않으면,error
가 발생한다.
const checkAgreement = () => {
// checkbox 가 check 되었는지 확인하는 함수이다.
const agreement = document.querySelector("#agreement");
// checkbox input 을 가져온다.
const checked = agreement.checked;
// checkbox 가 check 되었는지 확인한다.
// check 되었으면 true
// 아니면 false 이다.
if (!checked) {
// cheked 가 false 이면
// 해당 요소의 부모에
// <div class="error">동의해 주셔야 회원가입이 가능합니다.</div> 을 추가한다.
agreement
.parentElement
.insertAdjacentHTML(
"afterend", `
<div class="error">동의해 주셔야 회원가입이 가능합니다.</div>
`);
return checked; // false 를 반환한다.
} else {
return checked; // ture 를 반환한다.
}
}
이렇게 각 error
체크가 완료되었다.
이제 .signup__btn
클릭시 위 error check
함수를 실행하도록 만든다.
회원가입 버튼을 누를때, error 를 검증하고, 검증되면 회원가입이 되는 버튼을 설정하는 함수이다.
const signup = () => {
const $signupBtn = document.querySelector(".signup__btn");
// 회원가입 버튼을 가져온다.
$signupBtn.addEventListener("click", () => {
// 회원가입 버튼에 click 이벤트를 지정한다.
const checkArr = [checkInputs(), checkGender(), checkAgreement()];
// 각 error 검증 함수를 실행시킨후,
// boolean 값을 반환한 배열을 할당한다.
// 이렇게 하는 이유는, 각 함수를 if 문안에 실행시키고 and 연산자를 사용하면,
// 함수 호출시 문제가 생긴다.
// 만약 첫번째 지정한 함수가 false 를 반환하면, 나머지 함수는 실행되지 않는다.
// 그렇기에, 함수 실행을 보장하며, boolean 값들을 가질 수 있는
// 배열에서 실행시킨다.
if (checkArr[0] && checkArr[1] && checkArr[2]) {
// 배열 전부 true 라면, 아래의 alert 이 실행된다.
alert("가입을 축하합니다.");
} else {
// 배열중 한개라도 false 이면, 아래의 "가입안됨" error 가
// console 에 찍힌다.
console.error("가입안됨");
}
});
}
하지만 위의 코드대로 구현하면 분명 문제가 생긴다.
회원가입 버튼을 누를때 한번 검증에 실패한후, 재검증시 이전에 설정한 error
가 지워지지 않고
그대로 남아있기 때문이다.
특히, div.error
는 DOM
으로 만든 것이라 계속 만들어진다.
그렇기에 재검증시 기존의 error
를 전부 없애는 함수가 필요하다.
const removeError = () => {
const inputErrors = document.querySelectorAll("input.error");
// .error 가 있는 input 을 전부 가져온다.
const divErrors = document.querySelectorAll("div.error");
// div.error 인 div 를 전부 가져온다.
inputErrors.forEach(error => {
error.classList.remove("error");
// input 은 class 를 추가했기에 해당 class 만 제거하면된다.
// 각 input 에서 .error 만을 제거한다.
});
divErrors.forEach(error => {
error.remove();
// div.error 요소를 전부 없앤다.
});
};
이렇게 removeError
함수를 실행하면,
회원가입 버튼을 눌러 재검증시, 기존의 error
가 전부 삭제된다.
하지만 문제는 여기서 끝나지 않는다.
"회원가입" 되었다는 alert
이 발생한이후, 값이 그대로 남아있다.
이러한 값을 초기화할 필요가 있다.
그렇기에 맨 처음으로 초기화 시키는 함수를 작성한다.
const resetSignup = () => {
const $phoneInputs = document.querySelectorAll(".phone__wrapper input");
// .phone__wrapper 의 모든 input 을 가져온다.
const $bodyInputs = document.querySelectorAll(".input__wrapper input");
// .input__wrapper 의 모든 input 을 가져온다.
const $radios = document.getElementsByName("gender");
// name 이 gender 인 모든 radio input 을 가져온다.
const $checkbox = document.querySelector("#agreement");
// checkbox input 을 가져온다.
const $authNumberBtn = document.querySelector("#auth__number__btn");
// 인증 번호 버튼을 가져온다.
const $authCheckBtn = document.querySelector("#auth__check__btn");
// 인증 확인 버튼을 가져온다.
const $signupBtn = document.querySelector(".signup__btn");
// 회원가입 버튼을 가져온다.
$phoneInputs.forEach(input => {
// .phone__wrapper 의 input 을 순회하며,
input.disabled = false;
// 해당 input 을 활성화 시킨다.
input.value = "";
// 해당 input 의 value 값을 "" 으로 한다.
});
$bodyInputs.forEach(input => {
// .phone__wrapper 의 input 을 순회하며,
input.disabled = false;
// 해당 input 을 활성화 시킨다.
input.value = "";
// 해당 input 의 value 값을 "" 으로 한다.
});
$radios.forEach(radio => radio.checked = false);
// radio 를 순회하며 check 를 해제한다.
$checkbox.checked = false;
// checkbox 의 check 를 해제한다.
$authNumberBtn.previousElementSibling.textContent = "000000";
// 인증번호 버튼의 6자리 문자열을 "000000" 으로 초기화 시킨다.
$authCheckBtn.previousElementSibling.textContent = "3:00";
// 인증체크 버튼의 카운터를 "3:00" 으로 초기화 시킨다.
setDisabled($authNumberBtn);
// 인증번호 버튼을 disabled 한다.
setDisabled($authCheckBtn);
// 인증체크 버튼을 disabled 한다.
setDisabled($signupBtn);
// 회원가입 버튼을 disabled 한다.
}
이렇게 하면, 회원가입은 맨처음 상태로 초기화된다.
이제 이 2개의 함수를 실행할 수 있도록 signup
함수에 추가한다.
const signup = () => {
const $signupBtn = document.querySelector(".signup__btn");
// 회원가입 버튼을 가져온다.
$signupBtn.addEventListener("click", () => {
// 회원가입 버튼에 click 이벤트를 지정한다.
removeError();
// 기존 error 를 제거하는 함수
// 함수호출 순서는 중요하다.
// 버튼을 누르자 마자 error 가 제거된후,
// 밑의 checkArr 의 함수가 실행되도록 한다.
// 만약 checkArr 의 함수가 실행된 후 removeError 함수가 호출되면
// error 는 삭제된 상태로 화면에 나온다.
const checkArr = [checkInputs(), checkGender(), checkAgreement()];
// 각 error 검증 함수를 실행시킨후,
// boolean 값을 반환한 배열을 할당한다.
// 이렇게 하는 이유는, 각 함수를 if 문안에 실행시키고 and 연산자를 사용하면,
// 함수 호출시 문제가 생긴다.
// 만약 첫번째 지정한 함수가 false 를 반환하면, 나머지 함수는 실행되지 않는다.
// 그렇기에, 함수 실행을 보장하며, boolean 값들을 가질 수 있는
// 배열에서 실행시킨다.
if (checkArr[0] && checkArr[1] && checkArr[2]) {
// 배열 전부 true 라면, 아래의 alert 과 resetSignup이 실행된다.
resetSignup();
// 회원가입 창을 맨 처음으로 초기화 시키는 함수이다.
// 이 함수는 이미 검증이 성공한 상태이므로, 검증 성공시 실행되어도 된다.
alert("가입을 축하합니다.");
// 회원가입 축하 alert 이 실행된다.
} else {
// 배열중 한개라도 false 이면, 아래의 "가입안됨" error 가
// console 에 찍힌다.
console.error("가입안됨");
}
});
}
이렇게 회원가입 창을 만들어 보았다.
만든 결과물은 다음과 같다.
이렇게 오늘은 하루가 마무리되어 가는구나!!