목표
1. 로그인/회원가입 페이지를 Main 컴포넌트 하나로 통합한다.
2. 회원가입 Api 요청을 수행한다.
3. 게시판을 구성하고 로그인시 자동으로 게시판으로 이동하도록 한다.
페이지 이동없이 하나의 컴포넌트에서 필요한 요소만 변경하도록 구성할 것이다.
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가 필요하지 않다.
joinFail
은 loginFail
과 동일하게 생성된다.
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로 갱신한다.
어떤 페이지냐에 따라 다른 문구를 보여주기 위해 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>
);
}
회원 가입 페이지일 경우는 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);
회원가입에 사용할 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에서 확인할 수 있다.