본문 바로가기

[개인프로젝트] 개발 공부

[프로젝트] 10. 자동 로그아웃 (feat. 타이머 기능)

728x90
반응형

레퍼런스로 참고하는 사이트에서 1분 뒤 자동 로그아웃 기능을 보고 나도 해당 기능을 만들어보고 싶어졌다.

내가 구현한 페이지에선 로그인 시 JWT 토큰이 발급이 되고 여기서 Access Token은 30분 정도의 만료 시간을 가지고 있다. 이를 이용하여 자동 로그아웃 기능을 만들어보겠다.

굳이 토큰을 사용하지 않더라도 서버에서 로그인 유지 시간의 만료 기한 기준을 정해놓고 기준이 되는 만료기한을 클라이언트로 보내주는 식으로 구현을 해도 된다.

 

구현할 내용은 setInterval 타이머를 통해 시간을 계산하고 1분이 남았을 때 로그인 연장 또는 로그아웃 버튼이 있는 모달창을 띄어준다. 만일 모달 이벤트에 사용자가 반응을 하지 않으면 1분 뒤 자동으로 로그아웃이 되는 식으로 구현을 해주었다.

 

우선 로그인 시 Access Token의 만료 기한을 넣어주고 타이머 스위치로 사용할 Zustand store를 작성해 준다.

// useTokenExpiresStore.ts
import { create } from "zustand";

interface tokenExpiresStore {
    isTokenExpiresTimeStart: boolean;
    setIsTokenExpiresTimeStart: (time: boolean) => void;
    isTokenExpiresTimeBox: boolean;
    setIsTokenExpiresTimeBox: (box: boolean) => void;
    tokenExpiresTime: number;
    setTokenExpiresTime: (decrease: number) => void;
}

const useTokenExpiresStore = create<tokenExpiresStore>((set) => ({
    isTokenExpiresTimeStart: false,
    setIsTokenExpiresTimeStart: (time: boolean) =>
        set((state: {isTokenExpiresTimeStart: boolean}) => ({
            isTokenExpiresTimeStart: (state.isTokenExpiresTimeStart = time),
        })),
    isTokenExpiresTimeBox: false,
    setIsTokenExpiresTimeBox: (box: boolean) =>
        set((state: {isTokenExpiresTimeBox: boolean}) => ({
            isTokenExpiresTimeBox: (state.isTokenExpiresTimeBox = box),
        })),
    tokenExpiresTime: 0,
    setTokenExpiresTime: (decrease: number) =>
        set((state: {tokenExpiresTime: number}) => ({
            tokenExpiresTime: (state.tokenExpiresTime = decrease - 1000),
        })),
}));

export default useTokenExpiresStore;
export type { tokenExpiresStore };

 

각각 타이머를 시작하기 위한 state와 로그아웃 모달창을 위한 state, 계속해서 1초씩 감소하는 state로 구성하였다.

로그인 로직에서 로그인 성공 시 타이머를 true 시키고 token 만료 시간을 넣어준다.

 

어느 페이지에 있든 타이머는 돌아가야 하며, 로그아웃 모달을 띄어주기 위해 setInterval 타이머 기능은 최상단 컴포넌트인 App.tsx에서 구현해 주었다. 

setInterval은 매 시간 동안 브라우저와 통신하는 함수로 일정한 간격으로 setInterval 내에 있는 코드를 반복시킬 수 있다. 이를 통해 타이머 기능을 구현해 주는 것이다.

// App.tsx

...

useEffect(() => {
    if(isTokenExpiresTimeStart) {
        const timer = setInterval(() => { // 1초 단위로 setInterval 작동
            setTokenExpiresTime(tokenExpiresTime);
        }, 1000);

        if(tokenExpiresTime === 60000) { // 1분 남았을 경우 모달창 open
            setIsTokenExpiresTimeBox(true);
        }
        if(tokenExpiresTime === 0) { // 0초가 되면 모달창 close, 로그아웃
            axios({
                method: "POST",
                url: "/member/logout"
            }).then((res) => {
                if(res.data.result) {
                    removeCookie("refreshToken");
                }
            }).catch((err) => {
                console.log(err.message)
            })
            window.localStorage.removeItem("role");
            setIsTokenExpiresTimeBox(false);
            setIsTokenExpiresTimeStart(false);
            clearInterval(timer);
            navigate("/autoLogout");
            window.location.reload();
        }

        return ():void => {
            clearInterval(timer);
        };
    }
}, [tokenExpiresTime]);

...

 

60초가 남았을 때 바로 로그아웃과 로그인 연장 기능이 있는 로그아웃 알림 모달창을 띄어주며, 여기서 이벤트에 반응하지 않고 0초가 되면 데이터베이스와 쿠키에 있던 refreshToken과 localStorage에 넣어준 데이터가 있다면 모두 제거가 된다.

localStorage에 있는 데이터를 통해 로그인 여부를 판단하기에 데이터가 제거되면 자연스럽게 로그아웃이 되고 로그아웃 페이지로 이동이 된다.

 

728x90
반응형

 

로그아웃 알림 모달창은 아래 사진과 같이 출력이 된다.

 

남은 시간을 계산하여 초단위로 표현을 한 것이다. 코드 내에서는 1000ms 단위로 동작을 하지만 UI에서는 사용자가 이해하기 쉽게 초 단위로 표현을 해주어야 한다. 해당 소스코드는 아래와 같다.

// LoginExpiresNavigation.tsx

...

const LoginExpiresNavigation = () => {

    const {tokenExpiresTime} = useTokenExpiresStore();
    const second:string = String(Math.floor((tokenExpiresTime / 1000) % 61)).padStart(2, '0');

    ...

    return (
        <StyledLoginExpiresNavigation $isModal={isTokenExpiresTimeBox}>

            <div className="len-modal-view">
                <div className="len-time-box">{second}초</div>
                <div className="len-head-text">자동 로그아웃 예정입니다.</div>
                <div className="len-body-text">계속하시려면 새로고침 또는<br/>로그인 연장 버튼을 클릭해 주세요.</div>
                <div className="len-btn-section">
                    <button onClick={() => logout()} className="btn-logout">로그아웃</button>
                    <button onClick={() => extension()} className="btn-extension">로그인연장</button>
                </div>
            </div>

        </StyledLoginExpiresNavigation>
    )
}

...

 

간단해 보이지만 실제로 구현을 할 때는 reissue 부분에도 신경을 써야 했기에 구현하는데 시간이 좀 더 걸렸었고 App.tsx에 setInterval을 작성하다 보니 모든 페이지에서 초 단위로 새로고침이 되는 현상도 있었다.

대표적으로 지도 api를 구현한 페이지가 있었는데 setInterval 영향이 미쳐 지도가 계속 새로고침이 되었었다. 해당 문제는 useMemo를 통해 해결을 하였고 차후에 지도 api 부분을 구현할 때 추가로 작성하도록 하겠다.

 

 

 

 

728x90
반응형