ContextAPI를 이용한 로그인 상태 관리

기록일기📫·2021년 2월 14일
7

이번 포스팅에서는 React Router와 ContextAPI를 이용해서 로그인 한 User만 특정 페이지에 접근할 수 있도록 구현해보자!

💡 포스팅에서 작성한 코드는 react-router 공식 페이지에 나와있는 Auth 부분을 참고하여 작성되었습니다.

진행하는 프로젝트에서 로그인 기능 구현을 위해 react-router 공식 페이지를 참고하다가 ContextAPI를 사용한 예제코드를 보게 되었는데 생각보다 복잡해서 이해하느라 거의 어제 하루를 다 보냈다..😂

그래서 포스팅을 통해 정리해두려 한다!

ContextAPI?

우선 contextAPI에 대해 간단히 살펴보자. react 공식 문서에서 발췌한 contextAPI에 관한 설명은 아래와 같다.

context는 React 컴포넌트 트리 안에서 전역적(global)이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법입니다.

contextAPI를 이용하면 props없이 여러 컴포넌트에게 state를 전달할 수 있다.

컴포넌트들 간의 state를 공유할 수 있는 provider를 만들고, useContext hook을 사용하여 해당 state를 전달받는 방식으로 데이터를 공유한다.

진행 순서

전체적인 코드에 흐름을 함수 단위로 나누어서 설명하려 한다. 각각의 함수가 어떤 기능을 하는지 설명한 후에 전체적인 코드를 보며 흐름을 파악해보자!

fakeAuth

우선 유저 정보를 서버에 보내고 인증 결과를 받아오는 로그인 로직을 흉내내기 위해, fakeAuth라는 객체를 정의해서 사용할 것이다.

fakeAuth 객체는 내부적으로 로그인에 사용되는 signin, signout 함수를 가지고 있다.

login 함수 호출 시 실제 서버 통신과 유사한 환경을 구성하기 위해 setTimeout을 이용하여 비동기 로직을 falsy하게 구현했다.

0.1초 뒤에 callback 함수를 호출하는 방식으로 로그인 트릭을 구현한다. 😁

useProvideAuth

이제 로그인 로직을 생성했으니 fakeAuth 객체를 통해 user의 상태관리를 해줄 수 있도록 useProvideAuth custom hook을 만들자.

💡 custom hook이란?

custom hook이란 여러 컴포넌트에서 공통으로 사용되는 로직을 따로 빼서 hook으로 만드는 것이다. custom hook 컴포넌트는 'use'라는 키워드로 시작해야한다.

useProvideAuth hook에서는 user state를 이용하여 user를 초기화하고 fakeauth의 signin, signout 함수를 이용해서 user의 로그인 상태를 관리한다.

그리고 user, signin, signout 함수를 프로터피로 갖는 객체를 return해 준다. 다음에 ProvideAuth에서 해당 객체를 전역 상태로 공유함으로써 모든 컴포넌트에서 user 상태에 접근할 수 있도록 할 것이다.

ProvideAuth

ProvideAuth는 authContext의 Provider를 생성한다. 위에서 만든 useProvideAuth custom hook을 호출해서 user, signin, signout을 담고 있는 객체를 받아오자.

이제 children 컴포넌트에서 useContext(authContext)를 선언 하기만 하면,
auth.user, auth.signin, auth.signout를 이용해서 user를 관리 할 수 있게 된다! 😊

useAuth

전역 컨텍스트인 authContext를 사용하기 위해 useContext를 Wrapping해서 return하는 custom hook을 만들었다.

useContext(authContext)로 사용해도 되지만, 의미를 조금 더 명확하게 하기위해 useAuth로 Wrapping해주자.

앞으로의 코드는 해당 hook을 사용하여 전역 상태에 접근 할 것이다.

Routing

이제 라우팅 구조에 대해 살펴보자.

우선 ProvideAuth로 컴포넌트들을 감싸주어 모든 컴포넌트들이 user에 접근할 수 있도록 했다.

react-router에서 제공하는 Link 컴포넌트를 통해 public과 protected 페이지에 접근 할 수 있다.

switch를 통한 routing 부분 코드를 살펴보자. protected 페이지는 PrivateRoute로 한겹 감싸져 있다.

이는 로그인 안 한 유저를 걸러내기 위함인데 protected 페이지 접근시 Login 되지 않은 유저라면 login 페이지로 redirect 된다.

Private Route

제한된 페이지(protected page)를 접근하기 위한 routing 컴포넌트이다.

이 컴포넌트에서 contextAPI를 이용해 볼 것이다. useAuth를 통해 전역 context에서 현재 auth 정보를 전달받자.

auth.user를 확인 후 만약 authorization된 유저가 있다면 제한된 페이지를 보여주고 없다면 /login 페이지로 redirect한다.

Login

Login 컴포넌트는 login 하지 않은 user가 보게 될 페이지이다. login button을 제공하고 누르면 전역 context의 auth의 user정보를 update 하고 전에 있던 페이지로 이동시킨다.

react-router에서 제공하는 useHistory와 useLocation hook을 이용하면 historylocation 객체에 직접 접근할 수 있다. 자세한 사용법은 react-router 공식 페이지에 상세히 나와있으니 생략하겠다.

전체 코드

한번에 확인하고 싶으신 분들을 위해 전체 코드를 첨부한다. 편의상 하나의 파일에 작성하였다.

import React, { useContext, createContext, useState } from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect,
  useHistory,
  useLocation,
} from "react-router-dom";


export default function AuthExample() {
  return (
    <ProvideAuth>
      <Router>
        <div>
          <AuthButton />
          <ul>
            <li>
              <Link to="/public">Public Page</Link>
            </li>
            <li>
              <Link to="/protected">Protected Page</Link>
            </li>
          </ul>

          <Switch>
            <Route path="/public">
              <PublicPage />
            </Route>
            <Route path="/login">
              <LoginPage />
            </Route>
            <PrivateRoute path="/protected">
              <ProtectedPage />
            </PrivateRoute>
          </Switch>
        </div>
      </Router>
    </ProvideAuth>
  );
}

const fakeAuth = {
  isAuthenticated: false,
  signin(cb) {
    fakeAuth.isAuthenticated = true;
    setTimeout(cb, 100); // fake async
  },
  signout(cb) {
    fakeAuth.isAuthenticated = false;
    setTimeout(cb, 100);
  },
};


const authContext = createContext();

function ProvideAuth({ children }) {
  const auth = useProvideAuth();
  return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

function useAuth() {
  return useContext(authContext);
}

function useProvideAuth() {
  const [user, setUser] = useState(null);

  const signin = (cb) => {
    fakeAuth.signin(() => {
      setUser("User");
      cb();
    });
  };

  const signout = (cb) => {
    fakeAuth.signout(() => {
      setUser(null);
      cb();
    });
  };

  return {
    user,
    signin,
    signout,
  };
}

function AuthButton() {
  let history = useHistory();
  let auth = useAuth();

  return auth.user ? (
    <p>
      Welcome!{" "}
      <button
        onClick={() => {
          auth.signout(() => history.push("/"));
        }}
      >
        Sign out
      </button>
    </p>
  ) : (
    <p>You are not logged in.</p>
  );
}

function PrivateRoute({ children, ...rest }) {
  let auth = useAuth();

  return (
    <Route
      {...rest}
      render={({ location }) =>
        auth.user ? (
          children
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: location },
            }}
          />
        )
      }
    />
  );
}

function PublicPage() {
  return <h3>Public</h3>;
}

function ProtectedPage() {
  return <h3>Protected</h3>;
}

function LoginPage() {
  let history = useHistory();
  let location = useLocation();
  let auth = useAuth();

  let { from } = location.state || { from: { pathname: "/" } };
  let login = () => {
    auth.signin(() => {
      history.replace(from);
    });
  };

  return (
    <div>
      <p>You must log in to view the page at {from.pathname}</p>
      <button onClick={login}>Log in</button>
    </div>
  );
}

contextAPI를 이용해 보니 전역으로 상태를 공유해서 props drilling없이 상태를 관리할 수 있다는 점이 큰 장점인 것 같다.

얼른 redux를 공부하고 어떤 점이 다른지 살펴보아야 겠다.💪💪

또 공부하다가 https://usehooks.com/ 라는 페이지를 발견했는데 다양한 custom hook 예제를 제공한다. 관심 있으신 분들은 참고 하셔도 좋을 것 같다.

2개의 댓글

comment-user-thumbnail
2021년 11월 15일

리프레시하면 로그아웃되겠네요

1개의 답글