회원가입 페이지를 만들어 보아요.

Jhoon·2022년 12월 20일
0

회원가입 페이지를 만들어보아요

참고로, 공부의 목적으로 작성하는 내용이다.
배우는 입장에서 회원가입 페이지를 만드므로, 많이 미숙한 부분이 많을 것이다.
실수가 있을 수 있으니 양해 바란다.

이제부터 회원가입 페이지를 만들어본다.

codecamp 수강중 구현하는 과제가 있었는데, 재미있어서 리마인드차 다시 만들어본다.
참고로 button 을 만들어서 제출하는 과제가 있었는데, 그 부분도 같이 해결차 디자인도 다시 만들어 본다.

디자인도 Nenumorphism 으로 만들었다.
Nenumorphism 은 잘 사용하지 않는다고 하지만, 그냥 예뻐서 만들었다.

Figma 는 금방 익힐것 같기는 한데, 더 많이 살펴봐야겠다.
무엇보다 Design System 이 미친듯이 막힌다.
시간날때, 주말에 하루 날 잡아서 왠종일 만들어봐야겠다.

Nenumorphism은 그림자와 빛으로 각 요소를 표현한다.
잡스횽이 있을때, 기존의 Skeuomorphism을 많이 밀었는데, 단점은 너무 사물처럼 만들어야 해서 현재 트렌드에 맞지 않는다.

현재는 심플하며, 눈에 잘띄며, 공간감이 있으며, 의미론적인 디자인이 트렌드이다.
즉, 4박자가 딱 맞아 떨어져야 좋은 디자인이라 하드라..
Material Design 을 보자. 멋지다.

그 대체재로 Neumorphism 이 떠올랐으나, Flat Design 이 등장한다.
Flat Design 이후 너무 단순하다는 이유로 NeumorphismFlat Desing의 장점을 합친
Material Design 이 탄생한다.

Material Design 을 살펴보면 Elevation 이 그림자를 통해 요소의 depth 를 표현한다. 멋지다!!

Neumorphism 이 너무 멋지게 생겨 먹어서 이것저것 참고해서 만들어 보았다.
CSS 공부하면서 이것 저것 표현하는데 적절하다 생각이 들었다.

공부차 Figmabox-shadow 를 참고 하기는 했지만,
왠만해서는 직접 구현하려고 노력했다.

참고로 Neumorphism Generator 도 있었다.
굳이 직접 안만들어도 될것 같다.

만들어진 완성본은 아래와 같다.
회원가입 부분만 만들었으며, 나머지는 버튼에 대한 상태값을 나타내기 위해 옆에 두었다.



이제부터 회원가입 페이지를 만들어본다.
디자인을 먼저 살펴보자.

Design

InputButton Design 시 각 상태값은 다음과 같다.

상태값:   default focus hover active

참고로, Error 상태를 표현하는 Design 은 따로 만들지 않았다.

Button


Input


input 은 따로 active 시 상태값을 주지 않았다.
focus 된이후 글을 쓰기에 굳이 active 가 필요하다는 생각은 하지 않았다.

Radio 및 Checkbox


radio, checkboxcheckednon-checked 만 표현한다.
나머지 상태값을 만들면 오히려 조잡해지는 느낌이라, 2가지 상태 및 disabbled 상태만 만들었다.

참고로 radio 버튼이 디자인상과 완벽히 똑같이는 만들어 지지 않아, 약간 다르다!!

figma 로 만들때도, box-shadow 의 한계가 느껴져, photoshop 하듯 blur주고 여러 layer 를 겹쳐서 만들었는데, CSS 에서도 같은방법을 사용하기에는 무리가 있었다. ㅠㅠ

그렇다고 이미지로 넣고 싶지는 않아 최대한 비슷하게 만들어 본다.

완성된 디자인


이렇게 각 inputbutton 을 조합하여 회원가입 창을 만들었다.
이제 만들어진 회원가입을 구현해볼것이다.



HTML

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 사용을 염두에 두고 구조를 만들어 보았다.
각각의 정렬 기준이 되는 요소들은 전부 *__wrappergroup 화 시켰으며, *__wrapper 내부의 요소들역시 정렬되어야 하는 상황이면 group 화 시켰다.

*__wrapper 내부에 굳이 div.name 형식으로 한번더 group 화 시켰는데, 이는 추후 div.errorjavascript 를 통해 밑에 추가하기 위해 group 화 시킨것이다.

그러면 *__wrapper 내부에 div.namediv.error 두개 가 생성되며, 이를 flex: display; flex-direction: column; 으로 해주면 밑으로 예쁘게 정렬된다.

참고로, input 관련 요소에는 따로 div.name 형식으로 묶지 않았다.
input 요소는 background-color, colorplaceholdererror 상태를 표시할 예정이다.

사실 이렇게 만들고 후회한다.
placeholder 로 표현하기에, 기존의 value 값을 삭제해야만 했다. placeholder 로 만드는것은 사용자 편의성 상 좋지 않은 방법인듯 싶다.
*잘못 작성시 재입력할 수 있기에 value 는 그대로 있는것이 좋을것 같다.

하지만 일단 만들었으므로 그대로 진행한다!!



CSS

밑은 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;
 }

각 코드를 분할해 본다.


.neu__input

자식 inputneumorphism 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 valuex-offsety-offsetblurspreadcolor
none
optional
x축의 offsety축의 offsetblur 값
optional
그림자 확장 수치
optional
색상

Default value 는 몇가지로 나누어진다.

namedescription
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축 으로 -1pxrgba 값을 준다는 것이다. 이때 rgba 의 마지막 값 0.8opacity 이다. 또한 blur1px 만 주었는데, 이는 blur 가 넓게 퍼지지 않도록 하는 효과이다.

그럼 input 의 오른쪽 하단에 1px 의 하얀색 투명한 라인이 만들어진다.
이는 해당 인풋의 외곽을 선명하기 위한 아래선이라고 생각하면된다.

inset -1px -5px 2px #FFFFFF 은 오른쪽 아래 y축5px 만큼 준다.
5px의 선이 생기며, 2pxblur 를 주어 선이 명확하지 않도록 흐릿하게 번지는 느낌을 주었다.

inset 0px 2px 1px #EFF9FF#EFF9FF 의 색상을 왼쪽 상단의 y축 으로 2px 을 주고 blur2px 만큼주어 약간의 흐릿한 경계를 준다.
어두운 색상은 경계가 명확하게 나타나기에, 1px 이 아닌 2px 로 주었다.

inset 1px 4px 4px rgba(0, 33, 51, 0.3) 은 어두운 생상을 넓게 blur 를 주기위해 추가한 값이다. 다른 부분과는 다르게 x축 으로 1px 을 주었는데, 이는 왼쪽부분에 어두운 부분이 더 나와서 입체감있게 만들기 위함이다.

이렇게 box-shadow 를 겹치고 겹쳐서 해당 neu__input 을 표현했다.
나머지, active hover focus disabled placeholder 역시 전부 이러한 방식으로 만든것이다.

.inputerror 일때역시 표현하기 위해 배경색과 글자색을 지정한다.

.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

.btn class 를 가진 buttonneumorphism 버튼으로 만드는 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

전체 *__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 에서 backgroundlinear-gradient 를 사용해 주었으며, display: flex 를 사용하여 가운데 정렬에 사용한다.

flex-directionflex-start 로 주었는데, .headerh2 태그를 왼쪽으로 정렬하기 위해서 이다.

이때 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 에 그림자가 만들어지며, insetright, bottom 에서 그림자가 생긴다.

이렇게 그림자를 여러개 겹치고 겹치면, 마치 위의 .wrapper 처럼 위로 튀어나온것 같은 효과를 만들 수 있다.


상단 부분의 headerh2 를 둘러싼 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

.body__container 는 본 내용들을 다음 몸체 div 이다.

여기서는 지정해 놓고 사용은 안했다.
괜히 만들었다.


input__wrapper

name email 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__wrapperflex 를 주고 flex-direction: column 으로 준다.
.input__wrapper input 들을 세로로 정렬시켜준다.

.input__wrapper 내부의 input 들은 서로간에 여백을 주기위해 margin-bottom32px 로 준다.


phone__wrapper

핸드폰 인증할때 사용할 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__wrapperflex 를 주었으며 justify-contentspace-between 을 준다.

.phone__wrapper input들은 가로로 정렬되어 있으며, 양끝으로 정렬될 수 있도록 space-between 을 주는것이다.

.phone__wrapper input 은 고정값을 주기 위해 widthheight 의 값을 넣어준다.
안 넣어주면 width 값이 넘쳐서 보기 좋지 못해지더라..


auth__wrapper

핸드폰 인증 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-directioncolumn 을 준다.
주어진 div 를 세로로 정렬해야 한다.


gender__wrapper

성별 선택을 둘러싼 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;
}

여기서 중요한건 apparancenone 으로 두어 기본 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;
}

지정했을때, 위와 같다.
여기서 hoverbox-shadow 를 재지정하여, 밑으로 움푹 들어간 듯한 이미지로 변경하고,
checked 시 에는 background 색상을 #D0EEFF 으로 변경시켜, 클릭시 색을 변경시킨다.


agreement__wrapper

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__wrapperflex-directioncolumn 으로 주었는데,
이는 나중에 javascriptdiv.error.agreement__wrapper 의 자식으로 넣어줄것이다. 그때, 세로로 정렬되어야 하므로 column 으로 준것이다.


signup__wrapper

회원가입 버튼을 둘러싼 div 이다.

.signup__wrapper {
    display: flex;
    justify-content: center;;
    align-items: center;
}
.signup__wrapper .signup__btn {
    width: 100%;
}

signup__wrapper .signup__btnwidth: 100% 를 주었는데, 이는 해당 button.wrapper 너비 만큼 차지 하기 위해 준것이다.


error

error 되었을때 표현하기 위한 class 이다.

 /* .error */
 .error {
    color: #ff4b4b;
 }
 div.error {
    margin-top: 8px;

 }

내가 만든 회원가입 페이지는 input.errordiv.error 로 나뉘기에, 여기서 div.errormargin-top: 8px 을 준다.


Javascript

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();

위 내용을 하나씩 리마인드 해본다.


핸드폰 번호 입력

setFocus


핸드폰번호 입력 조건

. 핸드폰번호 입력시 첫번째 자리는 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 타입이어야 합니다!!");
    }
}

인증 구현

setAuth


인증 조건

. 회원가입 버튼은 인증되기전까지 비활성화 된다.
. 핸드폰 번호를 입력이 완벽하게 된 이후 인증번호 버튼이 활성화 된다.
. 인증번호 버튼이 활성화 된 후 누르게 되면 랜덤한 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


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


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


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 함수를 실행하도록 만든다.

signup

회원가입 버튼을 누를때, 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.errorDOM 으로 만든 것이라 계속 만들어진다.
그렇기에 재검증시 기존의 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("가입안됨");
        }
    });
}

이렇게 회원가입 창을 만들어 보았다.
만든 결과물은 다음과 같다.

이렇게 오늘은 하루가 마무리되어 가는구나!!

profile
익숙해지면 다 할수 있어!!

0개의 댓글