[프로젝트] 게시판 front 리펙토링 (2) login-join페이지 통합

rin·2020년 6월 2일
1

React

목록 보기
10/16
post-thumbnail

목표
1. 로그인/회원가입 페이지를 Main 컴포넌트 하나로 통합한다.
2. 회원가입 Api 요청을 수행한다.
3. 게시판을 구성하고 로그인시 자동으로 게시판으로 이동하도록 한다.

store 갱신하기

액션 타입 추가

페이지 이동없이 하나의 컴포넌트에서 필요한 요소만 변경하도록 구성할 것이다.

  • PAGE_CHANGE: 실제로 라우터를 통해 페이지가 전환되는 것은 아니지만 로그인 페이지와 회원가입 페이지를 토글하기 위한 액션이다.
  • JOIN_FAIL: 회원 가입에 실패한 경우 errorMessage state를 갱신하기 위한 액션이다.

modules/main/type.js에 아래 두 타입을 추가하였다.

PAGE_CHANGE : 'MAIN/PAGE_CHANGE',
JOIN_FAIL : 'MAIN/JOIN_FAIL'

액션 생성자 추가

modules/main/action.js에 아래 두 액션 생성 함수를 추가하였다.

export const pageChange = createAction(
    type.PAGE_CHANGE
)

export const joinFail = createAction(
    type.JOIN_FAIL, errorMessage => errorMessage
)

pageChange는 토글로 상태만 변경될 것이기 때문에 다른 argument가 필요하지 않다.
joinFailloginFail과 동일하게 생성된다.

리듀서 수정

import {handleActions} from 'redux-actions'
import type from './type';

const initialState = {
    accountId : null,
    isLogged : false,
    errorMessage : null,
    isLoginPage : true
}

export default handleActions({
        [type.LOGIN_SUCCESS] : (state, action) => ({
            ...state,
            accountId: action.payload,
            isLogged: true
        }),
        [type.LOGIN_FAIL] : (state, action) => ({
            ...state,
            isLogged: false,
            errorMessage: action.payload
        }),
        [type.PAGE_CHANGE] : (state, action) => ({
            ...state,
            errorMessage: null,
            isLoginPage: !state.isLoginPage
        }),
        [type.JOIN_FAIL] : (state, action) => ({
            ...state,
            errorMessage: action.payload
        })
    }, initialState
);

추가된 state : isLoginPage

PAGE_CHANGE액션(로그인페이지와 회원가입 페이지 사이 이동)이 발생할 때 마다 isLoginPage 상태가 토글되고, 에러메세지를 null로 갱신한다.

컴포넌트 수정

Main

어떤 페이지냐에 따라 다른 문구를 보여주기 위해 isLoginPage 상태값을 가져온다. 또한 이전에 Link를 이용하여 라우팅했던 부분을 클릭시 pageChange액션 생성 함수를 수행하는 것으로 변경할 것이다.

변경 전후 코드는 다음과 같다.

변경 전변경 후

Argument로 받아온 isLoginPage 상태값을 이용하여 true일 경우 로그인 페이지에 맞는 문구를, false일 경우 회원가입 페이지에 맞는 문구를 사용하도록 한다.

Grid item하위의 Link또한 herf 파라미터를 #로 변경하여 라우팅하지 않도록 막고, 대신 onClick={() => pageChange()}를 통해 isLoginPage를 토글시킨다.

Main 컴포넌트는 유지하고 일부분만 변경하기 때문에 Main 컴포넌트의 내장 상태인 accountId와 password는 페이지 전환 시에도 유지되었다. (이 값은 TextField의 value이므로 사용자 화면에서도 유지됨.) 따라서 isLoginPage가 변경될 때 마다(true<->false) 두 상태값을 초기화 시켜주는 hook을 추가하였다.

전체 코드는 다음과 같다.

import React, {useEffect, useState} from 'react';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import Box from '@material-ui/core/Box';
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
import VpnKeyIcon from '@material-ui/icons/VpnKey';
import Typography from '@material-ui/core/Typography';
import {makeStyles} from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';

function Copyright() {
    return (
        <Typography variant="body2" color="textSecondary" align="center">
            {'Copyright © '}
            <Link color="inherit" href="https://material-ui.com/">
                Your Website
            </Link>{' '}
            {new Date().getFullYear()}
            {'.'}
        </Typography>
    );
}

const useStyles = makeStyles((theme) => ({
    paper: {
        marginTop: theme.spacing(8),
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
    },
    avatar: {
        margin: theme.spacing(1),
        backgroundColor: theme.palette.secondary.main,
    },
    form: {
        width: '100%', // Fix IE 11 issue.
        marginTop: theme.spacing(1),
    },
    submit: {
        margin: theme.spacing(3, 0, 2),
    },
}));

export default function Main({handleClick, message, isLoginPage, pageChange}) {
    const classes = useStyles();

    const [accountId, setAccountId] = useState("");
    const [password, setPassword] = useState("");

    useEffect(() => {
        setAccountId("");
        setPassword("");
    }, [isLoginPage])

    return (
        <Container component="main" maxWidth="xs">
            <CssBaseline />
            <div className={classes.paper}>
                <Avatar className={classes.avatar}>
                    {isLoginPage? <LockOutlinedIcon /> : <VpnKeyIcon />}
                </Avatar>
                <Typography component="h1" variant="h5">
                    {isLoginPage ? "Sign in" : "Sign up"}
                </Typography>
                <Typography variant="subtitle1" gutterBottom>
                    {message}
                </Typography>
                <form className={classes.form} noValidate onSubmit={(event) => {event.preventDefault(); handleClick(accountId, password)}}>
                    <TextField
                        variant="outlined"
                        margin="normal"
                        required
                        fullWidth
                        id="email"
                        label="Email Address"
                        name="email"
                        autoComplete="email"
                        autoFocus
                        value={accountId}
                        onChange={event => setAccountId(event.target.value)}
                    />
                    <TextField
                        variant="outlined"
                        margin="normal"
                        required
                        fullWidth
                        name="password"
                        label="Password"
                        type="password"
                        id="password"
                        autoComplete="current-password"
                        value={password}
                        onChange={event => setPassword(event.target.value)}
                    />
                    <Button
                        type="submit"
                        fullWidth
                        variant="contained"
                        color="primary"
                        className={classes.submit}
                    >
                        {isLoginPage ? "Sign In" : "Sing Up"}
                    </Button>
                    <Grid container>
                        <Grid item xs>
                            <Link href="#" variant="body2">
                                Forgot password?
                            </Link>
                        </Grid>
                        <Grid item>
                            <Link href="#" variant="body2" onClick={() => pageChange()}>
                                {isLoginPage ? "Don't have an account? Sign Up" : "Already have an account? Sign In"}
                            </Link>
                        </Grid>
                    </Grid>
                </form>
            </div>
            <Box mt={8}>
                <Copyright />
            </Box>
        </Container>
    );
}

MainContainer

회원 가입 페이지일 경우는 joinApi 요청을 사용하는 함수를 추가한다. 현재 페이지 상태를 파악하여 (isLoginPage) Main 컴포넌트의 handleClick Argument에 적절하게 넘겨준다.

전체 코드는 다음과 같다.

import React, {useEffect} from "react";
import {connect} from 'react-redux';

import Main from "../components/Main";

import {joinApi, loginApi} from '../store/api/userApi';
import {loginSuccess, loginFail, pageChange, joinFail} from "../store/modules/main/action";


const MainContainer = ({accountId, isLogged, isLoginPage,  errorMessage, loginSuccess, loginFail, pageChange, joinFail}) => {

    const loginSubmit = async (id, password) => {
        var response = await loginApi(id, password);
        if (typeof response.data.code != "undefined"){
            loginFail(response.data.message);
        } else {
            loginSuccess(id);
        }
    }

    const joinSubmit = async (id, password) => {
        var response = await joinApi(id, password);
        if (typeof response.data.code != "undefined"){
            joinFail(response.data.message);
        } else {
            alert("회원 가입에 성공하셨습니다. 로그인 페이지로 이동합니다.");
            pageChange();
        }
    }

    return <Main
        handleClick={isLoginPage? loginSubmit : joinSubmit}
        message={errorMessage}
        isLoginPage={isLoginPage}
        pageChange={pageChange}
        />;
}

const mapStateToProps = state => ({
    accountId: state.main.accountId,
    isLogged: state.main.isLogged,
    errorMessage: state.main.errorMessage,
    isLoginPage: state.main.isLoginPage
})

const mapDispatchToProps = dispatch => ({
    loginSuccess: (accountId) => dispatch(loginSuccess(accountId)),
    loginFail: (errorMessage) => dispatch(loginFail(errorMessage)),
    pageChange : () => dispatch(pageChange()),
    joinFail: (errorMessage) => dispatch(joinFail(errorMessage))
})

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(MainContainer);

UserApi

회원가입에 사용할 joinApi 함수를 추가한다.

import axios from 'axios';
import { DOMAIN } from '../../static/constant';


export async function loginApi(accountId, password) {
    try {
        return await axios.post(DOMAIN+'/api/users?type=LOGIN',
                {accountId: accountId, password: password});
    } catch (error) {
        const response = { data : {
                code : error.response.status,
                message: error.message
            }};
        return response;
    }
}

export async function joinApi(accountId, password) {
    try {
        return await axios.post(DOMAIN+'/api/users',
                {accountId: accountId, password: password});
    } catch (error) {
        const response = { data : {
                code : error.response.status,
                message: error.message
            }};
        return response;
    }
}

서버 요청에 실패한 경우에도 응답값을 보내 출력할 수 있도록 로직을 변경하였다. 서버 상의 오류인 500 에러가 발생한 경우 다음처럼 출력된다.

전체 코드는 github에서 확인할 수 있다.

profile
🌱 😈💻 🌱

0개의 댓글