유니서베이 로그인
main
date
Jul 18, 2024
slug
unisurvey-login-flow
author
status
Public
tags
Development
summary
type
Post
thumbnail
category
Development
updatedAt
Oct 12, 2025 10:44 AM
유니서베이 마이그레션의 우선 순위가 높은 부분은 설문 통계를 보여주는 결과 페이지이다.
기존 기능은 Vue로 만들어진 플랫폼을 사용하고 결과 화면의 경우 Nextjs로 되어있다.
여기서 한 가지 문제가 있는데 어떻게 로그인을 유지하면서 다른 페이지로 이동을 할 것인가 고민이 되었다. 처음 생각했던 흐름은 아래와 같다.
클라 1 : Vue.js, 클라 2 : Nextjs. 백엔드 : Nestjs
클라 1 → 백엔드 → 토큰을 신규 발급 후 response header 에 쿠키 생성 → 서버에서 클라 2로 리다이렉트 를 생각했었는데 CORS가 발생
당연히 개발 시점에서 포트가 달라지니 다른 클라이언트로 전환을 하지 못하는 거고 그렇다면 어떻게 해야하나..
너무 내가 리다이렉트에 꽂혀서 다른 부분을 놓쳤던 것 같다. 그래서 그냥 단순한게 의식의 흐름대로 위 문제를 풀어나갔다.
우선 클라 1에서 클라 2로 넘어갈 때 이 사이에 토큰을 새로 발급 받는다. 이 때 해당 페이지에서만 위 과정이 이루어지니 라우터 가드에서 해결할 수 있을 것 같았다.
import Vue from 'vue'; import VueRouter, { Route, RouteConfig } from 'vue-router'; const routes: Array<RouteConfig> = [ // ...생략 { path: 'result/:id', name: 'surveyResult', component: Result, beforeEnter: resultRedirectGuard, meta: { projectAccess: true }, } ]
resultRedirectGuard 라는 라우터 가드를 생성했다.
// redirectGuard.ts import { NavigationGuardNext, Route } from 'vue-router'; import { ins } from '@/utils/axios'; export const resultRedirectGuard = async (to: Route, from: Route, next: NavigationGuardNext) => { try { const snum = to.params.id; const { data } = await ins.get(`/router/result-page/${snum}`); const { redirectUrl } = data; if (redirectUrl) { window.location.href = redirectUrl; } else { next(); } } catch (e) { console.error('잘못된 요청입니다.'); } };
라우터 가드에서 백엔드 서버로 리다이렉트를 시킬 URL을 요청한다. 이 때 쿼리스트링으로 token을 받는다.
백엔드에서 처리할 내용은 기존 토큰을 체크하고 유효한 토큰이라면 신규 토큰과 설문 번호를 담아서 암호화 처리한다.
// 컨트롤러는 그냥 서비스만 실행하니 생략.. // 기존 토큰 검증은 컨트롤러 가드에서 하였다. // 편의상 redirect url 하드코딩 (실제는 환경변수로 관리) @Injectable() export class RouterService { constructor(private readonly authService: AuthService) {} async createRedirectUrl(user: UserPayloadDto, SNUM: number): Promise<RedirectDto> { // 유저 정보로 토큰 생성 const token = await this.authService.createToken(user, SNUM); const encryptToken = await this.authService.tokenEncryption(token); return { redirectUrl: `http://localhost:3000/api/auth?token=${encryptToken}`, }; } }
클라 2의 api 라우터로 요청을 보냈다. 음.. 클라이언트로 직접 보낼까 하다가 암호화 된 토큰을 받아서 새로운 토큰을 발급 받아야 하는데 그럼 서버에서 하는 게 맞지 않을까라는 생각으로 api 라우팅을 사용하였다.
import type { NextApiRequest, NextApiResponse } from 'next'; import { axiosInstance } from '@unisurvey/services/lib'; import cookie from 'cookie'; type ResponseData = { message: string; }; export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) { const method = req.method; // 토큰 체크 if (method === 'GET') { const url = `/auth/token/verify`; const token = req.query.token as string; if (!token) { res.status(400).json({ message: '유효하지 않는 토큰 입니다. 로그인 정보를 다시 확인해주세요.' }); return; } const { data } = await axiosInstance.post(url, { token, }); const { token: newToken, payload } = data; res.setHeader('Set-Cookie', [ cookie.serialize('token', newToken.toString(), { httpOnly: true, // 클라이언트에서 접근할 수 없게 설정 secure: process.env.NODE_ENV === 'production', // 프로덕션 환경에서만 secure 플래그 설정 maxAge: 60 * 60 * 24 * 1, // 하루 path: '/', }), cookie.serialize('user', JSON.stringify(payload), { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 24 * 1, path: '/', }), ]); const redirectUrl = `/result/${payload.SNUM}`; res.redirect(302, redirectUrl); } }
api 라우팅에서 다시 백엔드로 토큰 검증을 진행하고 올바른 토큰이라면 해당 토큰을 decode 해서 신규 토큰과 함께 담겨져 있는 값을 클라2 서버로 전달하고 여기서 쿠키를 생성해서 사용하는 식으로 해보았다.
그럼 이제 middleware에 넘겨 받은 설문 번호가 존재 하는지 체크하고 존재하는 설문이라면 결과 화면으로 넘어가도록 하면 될 듯하다.
import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; const originPath = process.env.NEXT_PUBLIC_REDIRECT_URL; const HOST = process.env.NODE_ENV === 'production' ? process.env.NEXT_PUBLIC_PRODUCTION_HOST : process.env.NEXT_PUBLIC_DEVELOPMENT_HOST; export const isValidateProjectBySnum = async (SNUM: number, token: string) => { const url = `${HOST}/api/project/access/${SNUM}`; const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, }, }); const data = await response.json(); return data.result; }; export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const token = request.cookies.get('token')?.value; // 결과 페이지일 경우 검사 if (pathname.startsWith('/result/')) { const resultMatch = pathname.match(/^\/result\/(\d+)$/); // 유효하지 않을 경우 if (!resultMatch) return NextResponse.redirect(originPath); if (resultMatch) { const snum = resultMatch[1]; if (!snum || isNaN(+snum)) return NextResponse.redirect(originPath); const isValid = await isValidateProjectBySnum(+snum, token); if (!isValid) return NextResponse.redirect(originPath); } } return NextResponse.next(); } // 미들웨어가 실행될 경로 설정 const matcher = ['/((?!healthy|api|_next/static|_next/image|favicon.ico).*)', '/result/:snum*']; export const config = { matcher, };