6-3. 문자열 검증하기, 오류 보여주기

곧 회원가입을 구현해볼텐데요, 이메일/아이디 중복확인 및 회원가입 요청을 넣기 전에, 문자열을 먼저 검증하고 오류가 있다면 이를 사용자에게 알려주는 작업을 구현하겠습니다.

오류 보여주는 액션 준비

먼저, 화면에 표시 할 오류 내용을 설정하는 액션을 만들어봅시다.

src/redux/modules/auth.js

(...)

const SET_ERROR = 'auth/SET_ERROR'; // 오류 설정

(...)

export const setError = createAction(SET_ERROR); // { form, message }

const initialState = Map({
    register: Map({
        form: Map({
            email: '',
            username: '',
            password: '',
            passwordConfirm: ''
        }),
        exists: Map({
            email: false,
            password: false
        }),
        error: null
    }),
    login: Map({
        form: Map({
            email: '',
            password: ''
        }),
        error: null
    }),
    result: Map({})
});

// reducer
export default handleActions({
    (...)
    [SET_ERROR]: (state, action) => {
        const { form, message } = action.payload;
        return state.setIn([form, 'error'], message);
    }
}, initialState);

문자열 검증하기

문자열 검증은 정규식으로 해도 되지만, 이를 더욱 편하게 하기 위하여, 라이브러리를 사용하여 구현을 해봅시다. 문자열 검증을 쉽게 해주는 validator 라이브러리를 설치하세요.

$ yarn add validator

validator 로 문자열 검증을 할 때는, 다음과 같은 코드를 사용합니다:

import {isEmail, isLength, isAlphanumeric} from 'validator';

isEmail('foo@bar.com'); // true
isLength('foo', { min: 4, max: 15 }); // false
isAlphanumeric('foo123') // true
  • isEmail: 이메일 검증
  • isLength: 문자열 길이 검증
  • isAlphanumeric: 숫자 혹은 알파벳으로 이뤄졌는지 검증

더 많은 종류의 문자열을 검증하고 싶다면 공식 매뉴얼을 참조하세요.

회원가입 인풋 검증 함수 세트 객체 만들기

문자열을 어떻게 검증을 해야 할 지 배웠으니, 이제 검증함수로 이뤄진 객체를 컴포넌트내에서 정의하도록 하겠습니다. 그리고, setError 라는 컴포넌트 메소드를 정의하여 검증이 실패하면 이를 실행하도록 하겠습니다.

src/containers/Auth/Register.js

(...)
import {isEmail, isLength, isAlphanumeric} from 'validator';

class Register extends Component {

    setError = (message) => {
        const { AuthActions } = this.props;
        AuthActions.setError({
            form: 'register',
            message
        });
    }

    validate = {
        email: (value) => {
            if(!isEmail(value)) {
                this.setError('잘못된 이메일 형식 입니다.');
                return false;
            }
            return true;
        },
        username: (value) => {
            if(!isAlphanumeric(value) || !isLength(value, { min:4, max: 15 })) {
                this.setError('아이디는 4~15 글자의 알파벳 혹은 숫자로 이뤄져야 합니다.');
                return false;
            }
            return true;
        },
        password: (value) => {
            if(!isLength(value, { min: 6 })) {
                this.setError('비밀번호를 6자 이상 입력하세요.');
                return false;
            }
            this.setError(null); // 이메일과 아이디는 에러 null 처리를 중복확인 부분에서 하게 됩니다
            return true;
        },
        passwordConfirm: (value) => {
            if(this.props.form.get('password') !== value) {
                this.setError('비밀번호확인이 일치하지 않습니다.');
                return false;
            }
            this.setError(null); 
            return true;
        }
    }


    handleChange = (e) => {
        const { AuthActions } = this.props;
        const { name, value } = e.target;

        AuthActions.changeInput({
            name,
            value,
            form: 'register'
        });

        // 검증작업 진행
        const validation = this.validate[name](value);
        if(name.indexOf('password') > -1 || !validation) return; // 비밀번호 검증이거나, 검증 실패하면 여기서 마침

        // TODO: 이메일, 아이디 중복 확인
    }

    (...)

검증 함수를 만들고, handleChange 에서 인풋 name 에 따라 다른 함수들을 실행하도록 설정을 하였습니다. 코드를 작성하고 나서 이메일 부분에 잘못된 형식의 텍스트를 적어보세요. 그리고 리덕스 개발자 도구를 조회해보면 SET_ERROR 액션이 기록 될 것입니다.

에러 보여주기

이제 에러를 화면에 보여주겠습니다. AuthError 라는 컴포넌트를 만들어서 빨간색으로 글씨를 띄워준 다음에, 화면에 이 컴포넌트가 나타날 때 흔들리는 애니메이션을 줘보도록 하겠습니다.

우선, 흔들리는 애니메이션을 준비해보세요.

src/lib/styleUtils.js

import { css, keyframes } from 'styled-components';

(...)

export const transitions = {
    shake: keyframes`
        0% {
            transform: translate(-30px);
        }
        25% {
            transform: translate(15px);
        }
        50% {
            transform: translate(-10px);
        }
        75% {
            transform: translate(5px);
        }
        100% {
            transform: translate(0px);
        }
    `
};

shake 라는 keyframes 기반 애니메이션을 추가하였습니다.

이제, AuthError 컴포넌트를 만들어봅시다.

src/components/Auth/AuthError.js

import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import { transitions } from 'lib/styleUtils';

const Wrapper = styled.div`
    margin-top: 1rem;
    margin-bottom: 1rem;
    color: ${oc.red[7]};
    font-weight: 500;
    text-align: center;
    animation: ${transitions.shake} 0.3s ease-in;
    animation-fill-mode: forwards;
`;

const AuthError = ({children}) => (
    <Wrapper>
        {children}
    </Wrapper>
);

export default AuthError;

컴포넌트를 만든 다음엔 이를 인덱스에 추가하세요.

src/components/Auth/index.js

export { default as AuthWrapper } from './AuthWrapper';
export { default as AuthContent } from './AuthContent';
export { default as InputWithLabel } from './InputWithLabel';
export { default as AuthButton } from './AuthButton';
export { default as RightAlignedLink } from './RightAlignedLink';
export { default as AuthError } from './AuthError';

이제 다시 Register 로 돌아가서, 스토어에 있는 error 값을 연결시켜주고, 이 값에 따라서 방금 만든 컴포넌트를 보여줘봅시다. AuthError 컴포넌트는 버튼의 상단에 위치시킵니다.

src/containers/Auth/Register.js

import { AuthContent, InputWithLabel, AuthButton, RightAlignedLink, AuthError } from 'components/Auth';
(...)
class Register extends Component {
    (...)

    render() {
        const { error } = this.props;
        const { email, username, password, passwordConfirm } = this.props.form.toJS();
        const { handleChange } = this;

        return (
            <AuthContent title="회원가입">
                (...)
                <InputWithLabel 
                    label="비밀번호 확인" 
                    name="passwordConfirm" 
                    placeholder="비밀번호 확인" 
                    type="password" 
                    value={passwordConfirm}
                    onChange={handleChange}
                />
                {
                    error && <AuthError>{error}</AuthError>
                }
                <AuthButton>회원가입</AuthButton>
                <RightAlignedLink to="/auth/login">로그인</RightAlignedLink>
            </AuthContent>
        );
    }
}

export default connect(
    (state) => ({
        form: state.auth.getIn(['register', 'form']),
        error: state.auth.getIn(['register', 'error'])
    }),
    (dispatch) => ({
        AuthActions: bindActionCreators(authActions, dispatch)
    })
)(Register);

이메일검증에 실패하면 위와같이 오류가 뜹니다. 아직까지는, 제대로 이메일을 작성해도 에러가 사라지지 않을 것 입니다. 그 이유는, 에러를 없애주는 부분은 이메일 검증에서 이뤄질것이기 때문입니다.

이제 이메일/아이디 중복 체크 API 를 호출해봅시다.

src/containers/Auth/Register.js

(...)

class Register extends Component {
    (...)

    checkEmailExists = async (email) => {
        const { AuthActions } = this.props;
        try {
            await AuthActions.checkEmailExists(email);
            if(this.props.exists.get('email')) {
                this.setError('이미 존재하는 이메일입니다.');
            } else {
                this.setError(null);
            }
        } catch (e) {
            console.log(e);
        }
    }

    checkUsernameExists = async (username) => {
        const { AuthActions } = this.props;
        try {
            await AuthActions.checkUsernameExists(username);
            if(this.props.exists.get('username')) {
                this.setError('이미 존재하는 아이디입니다.');
            } else {
                this.setError(null);
            }
        } catch (e) {
            console.log(e);
        }
    }

    handleChange = (e) => {
        const { AuthActions } = this.props;
        const { name, value } = e.target;

        AuthActions.changeInput({
            name,
            value,
            form: 'register'
        });

        // 검증작업 진행
        const validation = this.validate[name](value);
        if(name.indexOf('password') > -1 || !validation) return; // 비밀번호 검증이거나, 검증 실패하면 여기서 마침

        // TODO: 이메일, 아이디 중복 확인
        const check = name === 'email' ? this.checkEmailExists : this.checkUsernameExists; // name 에 따라 이메일체크할지 아이디 체크 할지 결정
        check(value);
    }

    (...)
}

export default connect(
    (state) => ({
        form: state.auth.getIn(['register', 'form']),
        error: state.auth.getIn(['register', 'error']),
        exists: state.auth.getIn(['register', 'exists'])
    }),
    (dispatch) => ({
        AuthActions: bindActionCreators(authActions, dispatch)
    })
)(Register);

이제 이미 존재하는 아이디를 입력하면 위와 같이 오류가 뜰 것입니다. 현재 코드는 잘 작동하지만, 한가지 문제점이 있습니다. handleChange 메소드가 실행 될 때마다 실행되기 때문에, 이메일 / 아이디 문자열 조건을 충족하고 난 다음에는 입력을 받을 때 마다 네트워크 요청을 하게 됩니다.

로컬에서 작업할때는 빨라서 상관 없겠지만, 실제 서비스에서 이렇게 한다면 좀 비효율적이겠지요? 이에 대한 해결 방법은 사용자가 인풋을 입력하다가 멈추고 특정시간이 지나야 요청이 시작하게끔 구현을 하면 됩니다. 이를 직접 구현하려면 조금 코드가 난잡해질수도 있습니다. 걱정하지 마세요! 이를 도와주는 라이브러리가 있습니다. lodash 라는 라이브러리를 설치하세요

$ yarn add lodash

lodash 는 자바스크립트 유틸리티로서, 유용한 함수들이 많이 내장되어있으며, 함수형 프로그래밍을 할 때 유용하게 사용되는 라이브러리입니다.

이 라이브러리에 내장되어있는 함수중에서, debounce 라는 함수를 사용하면 특정 함수가 반복적으로 일어나면, 바로 실행하지 않고, 주어진 시간만큼 쉬어줘야 함수가 실행됩니다.

debounce 를 checkUsernameExists 와 checkEmailExists 에 적용해볼까요?

src/containers/Auth/Register.js

(...)
import debounce from 'lodash/debounce';

class Register extends Component {

    (...)

    checkEmailExists = debounce(async (email) => {
        const { AuthActions } = this.props;
        try {
            await AuthActions.checkEmailExists(email);
            if(this.props.exists.get('email')) {
                this.setError('이미 존재하는 이메일입니다.');
            } else {
                this.setError(null);
            }
        } catch (e) {
            console.log(e);
        }
    }, 300)

    checkUsernameExists = debounce(async (username) => {
        const { AuthActions } = this.props;
        try {
            await AuthActions.checkUsernameExists(username);
            if(this.props.exists.get('username')) {
                this.setError('이미 존재하는 아이디입니다.');
            } else {
                this.setError(null);
            }
        } catch (e) {
            console.log(e);
        }
    }, 300)

    (...)

debounce 할 함수를 감싸주고, 두번째 파라미터로 쉬어야 할 시간을 넣어주면 됩니다. 시간의 단위는 밀리세컨드 (ms) 입니다.

이렇게 하고나면, 함수가 호출되면 다음에 300ms 이후에 실행하도록 설정을 하고, 만약에 그 사이에 새로운 함수가 호출되면, 기존에 대기시켰던걸 없애고 새로운 요청을 대기시킵니다.

results matching ""

    No results matching ""