React & Redux로 로그인 웹페이지 만들기

DANO PARK·2022년 5월 28일
5
post-thumbnail

Megabyte School React 과정에서 API를 통해 가져온 정보로 로그인 페이지를 만들어보는 실습 과제를 수행했다.

API를 사용하는 방법은 독학이나 이번 과정에서 몇 번 해봤지만 아직까지 마냥 막막하고 손에 익지 않은 것 같다. 이때문에 이번 과제를 수행하는데 많이 해맸던 것 같다.

API... Reat... Redux 다 배운건데 어떻게 하더라...

알듯 말듯 될 듯 말듯 안되는 상황 속에 고전하고 있던 차에 수강생 한 분 께서 전체적으로 정리를 해주셨다. 덕분에 무사히 어떤 부분에서 이해가 부족했는지 알 수 있었고, 성공적으로 과제를 마칠 수 있었다.

프로젝트 준비하기

실습 과제는 아래와 같은 구조로 되어 있는 프로젝트다. LoginComponent.jsx는 아이디와 비밀번호를 입력할 수 있는 로그인 화면,MyPage.jsx는 로그인이 성공했을 경우
나타나는 페이지다.

실습 목표는 다음과 같다.

  • 아이디와 비밀번호를 state, input으로 관리할 것.
  • 로그인 버튼 클릭 시 빈 값이 하나라도 있다면, alert()submit event를 종료시킬 것.
  • axios를 통신하는 동안 버튼이 클릭되지 않도록 할 것.
  • axios 통신이 끝난 후 1.5초까지 'Loading...'을 출력할 것.

유저 정보는 다음과 같다.

  • ID : fastcampus
  • PW : 1234

API 명세서는 다음과 같다.

  • 반드시 body(obj)를 같이 보내줄 것.

    • body.id : user.id
    • body.pw : user.pw
  • 로그인 실패시

    • code : 400 = body 값이 비어 있을 때
    • code : 401 = 존재하지않는 id일 때
    • code : 402 = 비밀번호가 틀렸을 때
  • 로그인 성공 시

    • code : 200
    • useInfo = object, user의 데이터가 넘어옴.

main.jsx

main.jsx에 Provider와 store를 아래와 같이 설정해준다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

import { Provider } from "react-redux";
import store from "./reducer/store.js";

ReactDOM.createRoot(document.getElementById('root')).render(
    <Provider store={store}>
      <App />
    </Provider>
)

store.js

configureStore()로 스토어를 만들어 준다. 리듀서는 userSlice.js에서 가져와주자.

import { configureStore } from "@reduxjs/toolkit";
import userSlice from "./userSlice";

export default configureStore({
  reducer: {
    user: userSlice,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
});

LoginComponent.jsx

먼저 로그인 컴포넌트를 완성해주자. 제일 먼저 아이디 input과 비밀번호 input, 그리고 로그인 버튼을 만든다. 그리고 input에 입력한 값은 useState()로 관리한다. 그 다음 버튼을 클릭했을때, axios를 이용해 서버와 통신하여 입력한 input 값이 정확한지 확인하도록 해야한다.

// API
let body = {
	id,
    password
}
axios.post(LOGIN_API_KEY, body).then(res =>{***})

서버와 통신하기 위해서 위와 같은 코드를 작성한다. body는 id와 password 값을 받아서 서버로 전달해준다. 현재 아이디 input의 값과 비밀번호 input의 값은 각각 id와 password이란 이름으로 지정했으므로, body가 필요한 값과 input 값의 이름이 같다. 따라서 id = id, password = password이므로 위와 같이 하나씩만 작성하는 것이 가능하다.

body에 담긴 값은 axios.post()를 통해 LOGIN_API_KEY로 전달된다. 그리고 그 정보는 then()을 통해서 res로 받을 수 있다. console.log(res)를 해본다면 아래와 같은 정보가 뜰 것이다.

우리가 필요한 것은 data 부분인데, 현재 input에는 아무것도 입력하지 않았기 때문에 code : 400이 뜬 것을 볼 수 있다. 이것을 가지고 다음 화면으로 넘어갈지, submit 이벤트를 종료할지 선택할 수 있다. 코드가 400대라면 alert()를 제공하고 submit 이벤트를 종료 시킬 것이고, 코드가 200이라면 로그인 성공 화면으로 넘어가게 할 것이다.

코드가 200이 나오기 위해서는 위의 예시처럼 id 값을 fastcampus, password 값을 1234로 입력시켜줘야한다.

console.log(res.data)를 입력했을때 코드가 200일 경우 userInfo를 통해 추가로 정보가 제공된다. 이것을 가지고 로그인 성공화면에서 텍스트로 "Welcome 패스트캠퍼스!"라는 안내 문구를 제공할 것이다.

조건문을 통해 코드가 400대일 경우 alert() 후 이벤트 종료, 200일 경우 해당 정보를 userDispatch()를 통해 userSlice.jsloginUser() 리듀서로 넘겨주자. axios 통신이 끝나기 전 해당 조건문이 실행되면 제대로 동작하지 않기 때문에 then() 안에서 작성해야 한다.

.then(res => {
  const code = res.data.code;
  if (code === 400) {
  	alert("비어있는 내용입니다.");
  } else if (code === 401) {
  	alert("존재하지 않는 id입니다.");
  } else if (code === 402) {
  	alert("비밀번호가 일치하지 않습니다.");
  } else {
  	dispatch(loginUser(res.data.userInfo));
  }
});

무사히 userInfo 값을 리듀서에 전달 한 상황이다. 이제 버튼을 클릭했을때 "Loading..." 문구가 출력됐다가 이벤트까 끝나면 1.5초 뒤 사라져야하며, 그동안 버튼이 클릭되지 않도록 해야 한다.

로딩 메세지는 useEffect()를 사용해도 되지만, submit 이벤트가 발생할때 출력되도록 코드를 작성했다. 이 경우 then() 안에 setTimeout()을 통해 로딩 메세지가 axious 통신이 끝난 이후 1.5초 뒤 사라지게 하자.

그리고 버튼은 disabled={}에 들어갈 값을 useState()를 통해 관리해준다. 초기 값은 false, submit 이벤트가 발생하면 true, axious 통신이 끝나면 다시 false 값을 지정해주자.

위의 설명을 통해 아래와 같이 코드를 작성해보자.

import React, { useState } from "react"
import { useDispatch } from "react-redux";
import { loginUser } from "../reducer/userSlice.js";
import axios from "axios";
import "./Login.css"

function LoginComponent() {
    const dispatch = useDispatch();

    const [id, setId] = useState("");
    const [password, setPassword] = useState("");

    const [loading, setLoading] = useState(false);
    const [msg, setMsg] = useState("");

    const LoginFunc = (e) => {
        e.preventDefault();
        // Loading... 메세지 출력
        setMsg("Loading...");

        // API
        let body = {
            id,
            password
        }
        axios.post(LOGIN_API_KEY, body)
        .then(res => {
            // 2순위 통신이 끝나야 작동. 통신 이후 클릭이 되도록.
            setLoading(false);
            // Loading... 메세지가 통신이 끝난 후 1.5초 이후 없어짐.
            setTimeout(() => setMsg(""), 1500);
            // code = 데이터 상태
            const code = res.data.code;
            if (code === 400) {
                // 비어있는
                alert("비어있는 내용입니다.")
            } else if (code === 401) {
                // 존재하지 않는 id
                alert("존재하지 않는 id입니다.")
            } else if (code === 402) {
                // 비밀번호가 틀렸을때
                alert("비밀번호가 일치하지 않습니다.")
            } else {
                dispatch(loginUser(res.data.userInfo));
            }
        })
        // 1순위 로그인 버튼을 누르면 클릭이 안되도록.
        setLoading(true);
    }

    return (
        <>
            <h1>LoginComponent</h1>
            <form 
                onSubmit={LoginFunc}
                className="login-wrap"
            >
                <input
                        type="text" 
                        placeholder='아이디' 
                        className='id'
                        onChange={e => setId(e.target.value)}
                    />
                    <br />
                    <input 
                        type="password" 
                        placeholder='비밀번호' 
                        className='pw'
                        onChange={e => setPassword(e.target.value)} 
                    />
                <br />
                <button
                    disabled={loading} 
                    type="submit"
                    className='btn'
                >
                    로그인
                </button>
                <div
                    className='msg'
                >
                    {msg}
                </div>
            </form>
        </>
    )
}

export default LoginComponent

위 코드를 작성하면 아래와 같은 예제를 만들 수 있다.

아이디와 비밀번호를 모두 입력하지 않았을 경우

아이디는 정확하지만 비밀번호가 틀렸을 경우

비밀번호는 정확하지만 아이디가 틀렸을 경우

userSlice.js

userSlice.js에서는 loginUser()를 통해 초기 상태 값에 LoginComponent.jsx에서 받아온 userInfo 값을 집어 넣어 줄 것이고, clearUser()를 통해 받아온 값을 다시 비워주게 할 것이다.

먼저 createSlice()로 리듀서를 만들고 초기값을 다음과 같이 작성한다.

initialState: {
  name: "",
  id: "",
  isLoading: false, // optional
  isLogin: null,
 },

LoginComponent.jsx에서 받아온 userInfo의 값은 액션의 payload에 담겨져있다. 이것을
아래와 같이 loginUser()에서 initialState의 name과 id에 각각 넣어주자, 그리고 clearUser에는 넣어준 값을 다시 빈 값으로 만든다.

import { createSlice } from "@reduxjs/toolkit";

export const userSlice = createSlice({
    name: "user",
    initialState: {
        name: "",
        id: "",
        isLoading: false, // optional
        isLogin: null,
    },
    reducers: {
        // login 성공 시
        loginUser: (state, action) => {
            // name, id에 API 값 받아오기
            state.name = action.payload.name;
            state.id = action.payload.id;
            // state 변화를 알림
            return state;
        },
        // login 실패 시
        clearUser: (state) => {
            // name, id 값을 비워줌.
            state.name = "";
            state.id = "";
            // state 변화를 알림
            return state;
        },
    },
});

export const { loginUser, clearUser } = userSlice.actions;
export default userSlice.reducer;

App.jsx

LoginComponent.jsx와 Mypage.jsx 모두 App.jsx를 통해 화면에 출력되도록 구조를 설정했다. 그 이유는 App.jsx에서 특정 조건이 일어날 경우 한 컴포넌트만을 출력할 수 있도록 설정하기 위해서다. 아까 userSlice.js에서는 loginUser()가 실행될 경우 userInfo의 값이 state에 적용될 것이고, clearUser()가 실행된다면 state가 초기 값과 같은 빈 값으로 설정될 것이다. 이것을 아래와 같이 삼항연산자로 App.jsx에 작성하면 원하는 조건에 원하는 컴포넌트가 출력된다. 현재 state에서 변동이 일어나는 것은 id와 name이므로, 둘 중 하나를 선택해 작성한다.

import React from 'react';
import { useSelector } from "react-redux";
// import { BrowserRouter, Routes, Route } from 'react-router-dom';

import LoginComponent from './component/LoginComponent';
import MyPage from './component/MyPage';

function App() {
  const user = useSelector(state => state.user);

  return (
    <>
    {user.id !== "" ? <MyPage /> : <LoginComponent />}
    </>
  )
}

export default App

MyPage.jsx

Mypage.jsx에서는 useSelector()를 사용해서 userSlice.js에 있는 name 값을 가져와서 출력하고, useDispatch()를 통해서 버튼을 클릭하면 clearUser()를 실행시켜 값을 비워줘야 한다.

import React from 'react'
import { useSelector, useDispatch } from "react-redux";
import { clearUser } from "../reducer/userSlice.js";
import "./MyPage.css"

function MyPage() {
    const user = useSelector((state) => state.user);
    const dispatch = useDispatch();

    const LogoutFunc = () => {
        dispatch(clearUser(user));
    }
    return (
        <>
            <h1>MyPage</h1>
            <p>{`${user.name}(${user.id})`}님, 안녕하세요!</p>
            <button onClick={() => LogoutFunc()}>로그아웃</button>
        </>
    )
}

export default MyPage

위의 코드를 작성했을 경우 아래와 같은 예제가 출력된다.

끝.

profile
단오해서 단호박!

0개의 댓글