[React Toy Project] Router를 사용한 회원 인증 및 접근 제한

자몽·2021년 7월 17일
2

Toy-Project

목록 보기
1/13
post-thumbnail

시작하기에 앞서,

특징

  • 로그인/로그아웃/회원가입 기능이 존재한다.
  • 로그인 시에만 Profile 페이지에 접근 가능하다.
  • 서버 대신에 임시로 프론트단에 인증 모듈을 구현하였다.

주요 기술:

react-router,
async,
useState

참고 블로그: https://www.daleseo.com/react-router-authentication/

✈ 시작

프로젝트 파일 구조는 다음과 같다.

src

  • App.js: 리액트 라우터를 사용하기 위해 기본적인 준비

src>auth

  • auth.js: 서버를 대체할 인증 모듈
  • AuthRoute.js: 인증이 필요한 컴포넌트를 위해 구분

src>components

  • LoginForm.js: 로그인 폼(email,password)
  • LogoutButton.js: 로그아웃 버튼은 기능이 많아 따로 컴포넌트로 생성해 주었다.
  • Profile.js: 로그인(인증) 후 들어갈 수 있는 프로필 컴포넌트
  • RegisterForm.js: 회원가입 폼(name,email,password)

src>pages

  • About.js: 인증 없이도 들어갈 수 있는 평범한 페이지
  • Home.js: 메인 디렉토리

📕 App.js

설명을 위해 코드를 나눠서 설명하겠다.

App.js [1/3]

import React, { useState } from 'react'
import { Link, Route, Switch, BrowserRouter as Router } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Profile from './components/Profile';
import { signIn } from './auth/auth';
import AuthRoute from './auth/AuthRoute';
import LogoutButton from './components/LogoutButton';
import LoginForm from './components/LoginForm';
import RegisterForm from './components/RegisterForm';

react-router-dom 라이브러리에서 Link, Route, Switch, BrowserRouter as Router를 가져온다.
Link: 클릭 시 이동하는 url을 지정
Switch: Route중에 일치하는 첫 번째 요소 불러옴
BrowserRouter: url, ui를 동기화시킴

여기서는 BrowserRouter as Router를 통해 BrowserRouter 대신에 Router를 사용했다.(이름 변경)

App.js [2/3]

const App = () => {
  const [user, setUser] = useState(null);
  // authenticated: 로그인 상태 확인
  const authenticated = user != null;
    // 로그인, 로그아웃
  const login = ({ email, password }) => setUser(signIn({ email, password }));
  const logout = () => setUser(null);
  // 회원가입
  const [signUp, setSIgnUp] = useState(null);
  const signUpCompleted = ({ sign }) => setSIgnUp({ sign });
  return (
    <Router>
      <header>
        <Link to="/">
          <button>Home</button>
        </Link>
        <Link to="/about">
          <button>About</button>
        </Link>
        <Link to="/profile">
          <button>Profile</button>
        </Link>
        {authenticated ? (
          <LogoutButton logout={logout} />
        ) : (
          <Link to="/login">
            <button>Login</button>
          </Link>
        )}
        {authenticated ? (
          <></>
        ) :
          (signUpCompleted ? <></> :
            <Link to="/register">
              <button>Register</button>
            </Link>
          )
        }
      </header>

const authenticated = user != null; 부분이 가장 중요하다.
authenticated를 통해 로그인 상태를 확인하며,

authenticated == true이기 때문에, 조건 연산자를 통해서 로그인이 되었을 경우, 회원가입이 나타나도록 만들어 주었다.

회원가입도 위의 authentiacted와 유사하게 만들어졌으며,
로그인/ 로그아웃은 login, logout이 관여한다.

App.js [3/3]

      <hr />

      <main>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route
            path="/register"
            render={props => (
              <RegisterForm authenticated={authenticated} signUpCompleted={signUpCompleted} {...props} />
            )}
          />
          <Route
            path="/login"
            render={props => (
              <LoginForm authenticated={authenticated} login={login} {...props} />
            )}
          />
          <AuthRoute
            authenticated={authenticated}
            path="/profile"
            /*"render={props" 를 쓰는 이유
            컴포넌트에 props를 넘기기 위해 사용한다 */
            render={props => <Profile user={user} {...props} />}
          />
        </Switch>
      </main>
    </Router>
  );
}

export default App;

Route: 컴포넌트의 속성에 설정된 url과 현재 경로가 동일하면 컴포넌트 혹은 함수를 불러온다.

  • 단순히 컴포넌트만 받아오려면 <Route path="/" component={Sample}/>과 같이 사용하지만,
  • 컴포넌트에 props를 전달해주고 싶은 경우, render를 사용한다.
    ex) <Route render={props=>(<Sample {...props}/>

📙 LoginForm.js

import React, { useState } from "react";
import { Redirect } from "react-router-dom";

function LoginForm({ authenticated, login, location }) {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const onSubmit = (e) => {
        e.preventDefault();
        try {
            login({ email, password });
        } catch (err) {
            alert("Failed to login");
            setEmail("");
            setPassword("");
        }
    };

    const { from } = location.state || { from: { pathname: "/" } };
    if (authenticated) return <Redirect to={from} />;

    return (
        <form onSubmit={onSubmit}>
            <h1>Login</h1>
            <input
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                type="text"
                placeholder="email"
            />
            <input
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                type="password"
                placeholder="password"
            />
            <button type="submit" >Login</button>
        </form>
    );
}

export default LoginForm;

Redirect: 페이지를 다른 주소로 강제 이동시키는 것
여기서는 이 부분이 가장 중요하다.

const { from } = location.state || { from: { pathname: "/" } };
if (authenticated) return <Redirect to={from} />;

authenticated는 login에 값이 들어가면 App.js의 user state값이 변동되면서 true로 바뀌기 때문에, pathname: "/" 주소로 Redirect가 된다.


📒 LogoutButton.js

import React from "react";
import { withRouter } from "react-router-dom";

function LogoutButton({ logout, history }) {
    const onClick = () => {
        logout();
        history.push("/");
    };
    return <button onClick={onClick}>Logout</button>;
}

export default withRouter(LogoutButton);

history.push(): 특정 경로로 이동
withRouter: 라우트가 아닌 컴포넌트에서 라우터에서 사용하는 객체로, location, match, history 를 사용하려면, withRouter라는 HoC를 사용해야 한다.

로그아웃 버튼 클릭 시, logout()을 통해 user state값이 비워지고, / 경로로 이동하게 된다.


📗 Register.js

import React, { useState } from "react";
import { Redirect } from "react-router-dom";
import { users } from '../auth/auth';

function RegisterForm({ authenticated, history, location, signUpCompleted }) {
    const [name, setName] = useState("");
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const [text, setText] = useState("");

    const onSubmit = (e) => {
        e.preventDefault();
        try {
            users.push({ name, email, password })
            runTasks();
            signUpCompleted(true)
        } catch (err) {
            alert("Failed to register");
            setEmail("");
            setPassword("");
        }
    };

    function loading(num) {
        const promise = new Promise((resolve, reject) => {
            setTimeout(() => {
                const result = num + 1;
                if (result > 5) {
                    const e = new Error('over loading');
                    return reject(e);
                }
                resolve(result);
            }, 500)
        });
        return promise;
    }
    async function runTasks() {
        try {
            let result = await loading(0);
            setText('[1/4]회원가입중.');
            await loading(result++);
            setText('[2/4]회원가입중..');
            await loading(result++);
            setText('[3/4]회원가입중...');
            await loading(result++);
            setText('[4/4]회원가입 완료 !');
            await loading(result++);
            history.push("/");
        } catch (e) {
            console.log(e);
        }
    }

    const { from } = location.state || { from: { pathname: "/" } };
    if (authenticated) return <Redirect to={from} />;
    return (
        <form onSubmit={onSubmit}>
            <h1>Register</h1>
            <input
                value={name}
                onChange={(e) => setName(e.target.value)}
                type="text"
                placeholder="name"
            />
            <input
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                type="text"
                placeholder="email"
            />
            <input
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                type="password"
                placeholder="password"
            />
            <button type="submit">Register</button>
            <div>{text}</div>
        </form>
    );
}

export default RegisterForm;
  • users는 사용자 정보가 저장된 객체인데, 여기에 push를 통해 사용자 정보를 넣어준다.
  • signUpCompleted를 통해 회원가입이 완료되면 회원가입 버튼을 없애 주는 기능을 넣었다.
    runTasks()는 비동기 함수를 호출해, 회원가입 버튼을 눌렀을 때, 0.5초간(500) '회원가입중..'이라는 메세지를 띄우고 이후에 history.push("/");를 통해 메인 페이지로 전환된다.

📘 Profile.js

import React from 'react'

const Profile = ({ user }) => {
    const { email, password, name } = user || {};
    return (
        <div>
            <h1>{name}'s Profile</h1>
            <div>Email: {email}</div>
            <div>Password: {password}</div>
        </div>
    )
}

export default Profile

프로필 컴포넌트는 인증 후에만 접근 가능하며, 이는 AuthRouter.js에서 자세히 다룰 예정이다.


📕 auth.js

export const users = [
    { email: "a@a.a", password: 'aaa', name: 'A' },
    { email: "b@b.b", password: 'bbb', name: 'B' },
    { email: "c@c.c", password: 'ccc', name: 'C' },
];
export function signIn({ email, password }) {
    const user = users.find(
        (user) => user.email === email && user.password === password
    );
    if (user === undefined) throw new Error();
    return user;
}

사용자 정보가 저장된 users 객체와,
users의 정보와 로그인시에 입력된 정보와 올바른지 비교하는 signIn함수가 들어있다.


📙 AuthRouter.js

import React from 'react';
import { Route, Redirect } from 'react-router-dom';

const AuthRoute = ({ authenticated, component: Component, render, ...rest }) => {
    return (
        <div>
            <Route
                {...rest}
                render={(props) =>
                    authenticated ? (
                        render ? (
                            render(props)
                        ) : (
                            <Component {...props} />
                        )
                    ) : (
                        <Redirect
                            to={{ pathname: "/login", state: { from: props.location } }}
                        />
                    )
                }
            />
        </div>
    )
}

export default AuthRoute

상당히 복잡하게 느껴질 수도 있지만 순서대로 살펴보면 생각보다 간단하다.
1. <Router>는 렌더링할 컴포넌트를 받는다.
2. authenticated가 참인 경우, render(컴포넌트에서 props 전달)인 경우와 일반 component인 경우를 나누어 렌더링 해준다.
3. authenticated가 겨짓인 경우, <Redirect>를 통해 로그인 경로로 이동시킨다.


📒 About.js

import React from 'react'

const About = () => {
    return (
        <div>
            <h1>About</h1>
            <p>How am I?</p>
        </div>
    )
}

export default About

📘 Home.js

import React from 'react'

const About = () => {
    return (
        <div>
            <h1>About</h1>
            <p>How am I?</p>
        </div>
    )
}

export default About

📌 기능 / 개선할 점

css를 넣지 않았으며, 다음과 같이 동작한다.

기능

5개의 링크(버튼)가 존재한다.

  • 비로그인시, 로그인 버튼, 회원가입 버튼이 보인다.
  • 이때 회원가입을 완료한 경우 회원가입 버튼은 사라진다.
  • 로그인을 하면, 로그인 버튼 대신 로그아웃 버튼이 생기고, 회원가입 버튼은 사라진다.

개선할 점

  • 상태관리를 리덕스로 바꿔볼 필요가 있을 것 같다.
  • 백엔드와 통신이 어려워 프론트엔드 단에 auth를 구성했는데, json-server를 사용해서라도 백엔드와의 api 통신이 필요할 것 같다.
  • users.push()로 회원가입을 해도 실제 코드에는 반영되지 않고, 해당 웹에서만 동작하는 문제가 있다.
  • 새로고침시 초기화(로그인 풀림)가 되는 문제가 있다.

추후, 이를 모두 개선할 예정에 있다.

이해를 돕기 위해서
전체 코드가 있는 깃 저장소: https://github.com/OseungKwon/practice-react/tree/main/user_confirm

profile
꾸준하게 공부하기

0개의 댓글