[2024.01.22] 로그인 여부에 따른 페이지 접근 제한 구현(Recoil-Persist)

아스라이지새는달·2024년 2월 7일
2
post-thumbnail

어쩌다 보니 프론트엔드도 하게 되었다. 팀원 중 한 명이 교환학생 중이라 귀국할 때까지 프로젝트를 잠정 중단하였는데 이대로 가만히 두기에는 싫어서 내가 조금이나마 진행을 하기 시작했다. 23년 2월에 React관련 아카데미를 수강한 적이 있어서 이 때의 경험을 살려 기능적인 부분만 진행하고 있다.
오늘은 React로 프론트엔드를 하면서 고민을 많이 했던 로그인 여부에 따른 페이지 접근 제한에 대해 포스팅 하려고 한다.

🏞️ 배경 및 구현 방향

페이지를 구현하면서 로그인 여부에 따라에 접근이 가능한 페이지를 나누었다.
어떤 페이지는 로그인이 되어있어야 접근이 가능하도록 해야했고 어떤 페이지는 로그인이 되어있으면 접근이 불가하도록, 즉 로그인이 되어있지 않아야 접근이 가능하도록 구성해야 했다.

정리해보면 다음과 같이 구현하고자 했다.

페이지조건리다이렉션
루트 페이지(로그인 전)로그인한 상태에서는 접근 불가능-
루트 페이지(로그인 후)로그인한 상태에서만 접근 가능-
로그인 페이지로그인한 상태에서는 접근 불가능루트 페이지
비밀번호 찾기 페이지

이렇게 구현하기 위해 로그인 여부를 판단하는 state를 사용이 불가피 했는데 이를 컴포넌트의 props로 전달하고 각 컴포넌트에서 여부를 판별하자니 코드가 지저분해지는 경험을 하였다. 간단한 방법을 찾기 위해 구글링을 하였고 Recoil에 대해 알게 되어 Recoil을 사용하기로 하였다.


❓ Recoil

Recoil은 React에서 사용할 수 있는 전역 상태 관리 라이브러리이다.

React에서 상위 컴포넌트(Component)에서 하위 컴포넌트로 데이터를 전달할 때 일반적으로 props를 통해 전달한다. 이 때 props drilling이 일어날 수 있다. props drilling은 props를 통해 데이터를 전달하는 과정에서 중간 컴포넌트는 그 데이터가 필요하지 않음에도 자식 컴포넌트에 전달하기 위해 props를 전달해야하는 과정을 말한다. props drilling이 무조건적으로 나쁜 것은 아니지만 어플리케이션의 규모가 커진다면 해당 props의 뿌리를 추적하기 어려워지고 이로 인해 유지보수가 힘들어진다는 단점이 있다.

이 props drilling을 해결하기 위해 전역 상태 관리 라이브러리를 사용하는데 Recoil이 이 중 하나이다.

설치

Recoil은 다음의 명령어로 설치 가능하다.

npm install recoil

RecoilRoot

Recoil을 사용하기 위해서는 어플리케이션의 최상위 컴포넌트에 RecoilRoot 컴포넌트를 명시해야 한다.

나는 react + vite 환경에서 프로젝트를 진행 중이기에 main.jsx 에서 작성해주었다.

// main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { RecoilRoot } from 'recoil'
import App from './App.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
)

Atom

atom은 상태(state)의 단위이며 업데이트와 구독이 가능하다. atom이 업데이트 되면 각각 구독된 컴포넌트는 새로운 값을 반영하여 리렌더링 된다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.

나는 로그인 여부 상태를 전역적으로 관리할 예정이기에 이를 atom으로 만들어주었다.

// ./utils/recoilState.js

import { atom } from 'recoil';

export const userInfoState = atom({
    key: "isLogin",
    default: false,
});

atom을 만들 때에는 키와 기본값을 요구하는데 이 때 키는 고유해야 한다.

Recoil에는 atom이라는 기본 상태 외에도 selector라는 파생 상태도 존재하는데 이 부분은 사용하지 않아서 자세한 설명은 생략했다. 궁금하다면 다음을 참고하자. 잘 설명된 포스트이다.

사용

사용할 때는 React의 useState와 비슷하게 사용하면 된다. 사용할 recoil state와 recoil 기능을 import하고 코드를 작성해준다.

LoginPage

로그인 페이지에서는 로그인에 성공하면 isLogin이라는 atom을 true로 세팅하도록 하였다.

// LoginPage.jsx

...
import { useRecoilState } from "recoil";
import { userInfoState } from '../utils/recoilState';


const LoginPage = () => {
    const [, setIsLogin] = useRecoilState(userInfoState);

    const [studentId, setStudentId] = useState("");
    const [password, setPassword] = useState("");

    const handleOnKeyPress = (e) => {
        if(e.key === "Enter") {
            handleLoginButtonClick();
        }
    };

    const handleStudentIdChange = (e) => {
        console.log("학번 입력");
        const { value } = e.target;
        setStudentId(value);
    };

    const handlePasswordChange = (e) => {
        console.log("패스워드 입력");
        const { value } = e.target;
        setPassword(value);
    };

    const handleLoginButtonClick = () => {
        // console.log("버튼 누름");

        const option = {
            method: "POST",
            url: "/api/member/login",
            data: {
                studentId: studentId,
                password: password,
            }
        };

        axios(option)
            .then(({ data }) => {
                console.log(data);
                setIsLogin(true);
                
                document.location.replace("/");
            })
            .catch((error) => {
                alert("아이디가 존재하지 않거나, 아이디 또는 비밀번호를 잘못 입력하셨습니다.");
                console.log(error);
            })

    };

    const handleFindPwButtonClick = () => {
        document.location.href = "/findpw";
    };

    return (
        <div>
            <LoginComponent
                handleOnKey={handleOnKeyPress}
                handleStudentId={handleStudentIdChange}
                handlePassword={handlePasswordChange}
                handleLoginClick={handleLoginButtonClick}
                handleFindPwClick={handleFindPwButtonClick}
            ></LoginComponent>
        </div>
    )
}

export default LoginPage

RootPage

루트 페이지에서는 로그인 여부에 따라 조건부 렌더링을 하도록 하였고 로그인된 상태에서 로그아웃을 하면 isLogin이 다시 false가 되도록 하였다.

// RootPage.jsx

...
import { useRecoilState } from "recoil";
import { userInfoState } from '../utils/recoilState';

const RootPage = () => {
    const [isLogin, setIsLogin] = useRecoilState(userInfoState);

    const handleLoginButtonClick = () => {
        document.location.href = "/login";
    };

    const handleLogoutButtonClick = () => {
        if(confirm("로그아웃 하시겠습니까?")) {
            setIsLogin(false);
          
            document.location.replace("/");
        }
    };
    
    if (isLogin) { // 로그인이 되어있는 경우의 rendering
        return (
            <div>
                <p>This is Main page. After Login.</p>
                <button type='button' onClick={handleLogoutButtonClick}>로그아웃</button>
            </div>
        )
    } else { // 로그인이 되어있지 않은 경우의 rendering
        return (
            <div>
                <p>This is Root page. Before Login.</p>
                <button type="button" onClick={handleLoginButtonClick}>로그인</button>
            </div>
        )
    }
}

export default RootPage

App.jsx

이 부분이 구현하고자 했던 페이지 접근 제한의 핵심이다.
useRecoilState로 isLogin 값을 받아오고 이를 삼항연산자와 결합해 로그인을 해야 접속 가능한 페이지와 로그인을 하면 접속이 불가능한 페이지로 나누었다.

즉 AdminPage(임시 페이지)는 로그인을 해야만 접속이 가능했고 LoginPage와 FindPasswordPage는 로그인을 하면 접속이 불가능한 페이지이다.

로그인을 하지 않았는데 접근 불가능한 페이지에 접속한 경우 또는 로그인을 하였는데 접속 불가능한 페이지에 접속한 경우에는 루트 페이지로 리다이렉팅 되도록 하였다.

// App.jsx

...
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { useRecoilState } from "recoil";
import { userInfoState } from "./utils/recoilState";


function App() {
  const [isLogin] = useRecoilState(userInfoState);

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<RootPage />}></Route> {/* 로그인 여부와 상관없이 접속 가능 */}
        {isLogin ?
          <> // 로그인이 되어있지 않으면 접속이 불가한 페이지
            <Route path="/admin" element={<AdminPage />}></Route>
          </>
          : // 로그인이 되어있으면 접속이 불가한 페이지
          <>
            <Route path="/login" element={<LoginPage />}></Route>
            <Route path="/findpw" element={<FindPasswordPage />}></Route>
          </>
        }
        <Route path="*" element={<Navigate to="/" />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

테스트

작성을 마치고 실행을 해보았다.

문제점

뭔가 이상하다. 의도한 대로라면 로그인 후 루트 페이지인 This is Main Page. After Login. 라는 문구와 로그아웃 버튼이 있는 화면이 나와야 하는데 로그인 전 루트 페이지가 나왔다.
자세히 보면 로그인 후 루트 페이지를 잠깐 접속했다가 바로 로그인 전 루트 페이지로 이동하는 것을 확인할 수 있었다.

문제를 해결하기 위해 로그인에 성공하면 루트 페이지로 리다이렉팅 하는 부분을 주석 처리하고 콘솔로 isLogin의 값을 찍어 확인해보기로 했다.

분명 로그인에 성공하였을 때는 isLogin이 true로 찍히는 것을 볼 수 있었는데 새로고침을 하면 다시 false로 찍히는 것을 볼 수 있다. 새로고침을 했을 때 로그인이 풀려버리면 로그인 여부에 따라 접근 가능한 페이지와 불가능한 페이지를 확인할 수 없기 때문에 우선 이 문제를 해결해야 한다.

루트 페이지로 리다이렉팅 하는 부분을 주석 처리했음에도 로그인 성공 시 루트 페이지로 이동하는 것은 App.jsx에서 로그인을 하였는데도 접속 불가능한 페이지에 접근했을 때 루트 페이지로 리다이렉팅 하도록 했기 때문이다.

구글링을 해보니 나와 같은 문제를 겪고있는 사람이 많았고 이를 해결하기 위해서는 state를 localStorage 혹은 sessionStorage에 저장하는 Recoil-Persist를 사용해야 한다는 것을 알게되었다.


❗️ Recoil-Persist

Recoil-Persist는 앞서 말한 것과 같이 리렌더링될 때 state가 초기화되는 문제를 해결하기 위해 사용하는 라이브러리이다.

설치

Recoil-Persist는 Recoil과 비슷하게 설치한다.

npm install recoil-persist

사용

Recoil에서 설정했던 atom을 살짝만 수정하면 된다.

import { atom } from 'recoil';
import { recoilPersist } from 'recoil-persist';

const { persistAtom } = recoilPersist({
    key: "sessionStorage",
    storage: sessionStorage,    
});

export const userInfoState = atom({
    key: "isLogin",
    default: false,
    effects_UNSTABLE: [persistAtom],
});

나는 브라우저가 종료되거나 탭이 닫히면 로그인이 풀리도록 구현하고 싶었기에 sessionStorage를 사용했는데 localStorage를 사용하고 싶다면 sessionStorage 자리에 localStorage를 작성하면 된다.

테스트

새로고침을 해도 로그인이 풀리지 않는 것을 확인하였으니 이제 로그인 여부에 따라 접근 가능한 페이지와 불가능한 페이지에 접속했을 때 의도한 대로 동작하는지 확인해 볼 차례이다.

로그인 하지 않았을 때

로그인 하지 않았을 때는 AdminPage에는 접근 불가하고 LoginPage와 FindPasswordPage에는 접근 가능하다.

정상적으로 LoginPage와 FindPasswordPage에는 접근이 가능하지만 AdminPage에 접근하려고 했을 때 RootPage로 리다이렉팅 하는 것을 볼 수 있다.

로그인 하였을 때

로그인 하였을 때는 AdminPage에는 접근이 가능하지만 LoginPage와 FindPasswordPage에는 접근이 불가하다.

이 또한 정상적으로 AdminPage에는 접근이 가능하지만 LoginPage와 FindPasswordPage에 접근하려고 했을 때 RootPage로 리다이렉팅 하는 것을 볼 수 있다.


📌 마치며

로그인을 구현하는 것이 간단할 줄 알았는데 실제로 구현을 해보니까 백엔드, 프론트엔드 모두 이것 저것 고려할 게 많았다. 백엔드만 경험했으면 생각하지 못했을 부분들을 프론트엔드를 경험해 봄으로써 좀 더 식견을 넓힐 수 있었다.


🔍 Reference

https://dawonny.tistory.com/406

https://recoiljs.org/ko/docs/introduction/motivation/

https://dawonny.tistory.com/388

https://playit.tistory.com/15#google_vignette

https://heycoding.tistory.com/84

https://velog.io/@leemember/React-recoil-persist-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-localStorage%EC%99%80-sessionStorage%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0

profile
웹 백엔드 개발자가 되는 그날까지

0개의 댓글