ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 미들웨어(프록시)로 유연하게 처리 해보기
    프론트엔드 2026. 6. 21. 22:31

    다시 한번

    - 더 해빗(TheHabit)이라는 습관 형성 챌린지 서비스를 다시 만들면서 겪은 이야기입니다.

    - 온보딩 페이지를 구현하면서 고려하지 못한 부분에 대해서 보완해보고자 프로젝트를 다시 시작했습니다.

     

    - 과거에 왜 하지 않았을까 생각해봤을때 딱 아래 사진과 똑같았습니다. 

    - 예외 처리 혹은 다른 여러가지 상황을 고려하지 못했던거 같습니다. 

    - 일단 작동하니 넘겼던 과거를 후회하면서...

     

     

     

    - 여러 예외 상황 등을 바로 잡아보고자 합니다. 

     

     

    과거의 구현이 어떻게 되었고 현재 어떻게 수정할지에 대해

    // 쿠키를 날것으로 직접 읽었음
    const token = req.cookies.get('next-auth.session-token')?.value;
    console.log('pathname', pathname); // console.log도 남아있었음
    
    if (token && pathname.startsWith('/onboarding')) {
      return NextResponse.redirect(new URL('/user/dashboard', req.url));
    }
    if (!token && pathname.startsWith('/user')) {
      return NextResponse.redirect(new URL('/login', req.url));
    }
    
    온보딩 컴포넌트
    // 완료 기록 자체가 없었음
    const handleNext = () => {
      if (currentStep < ONBOARDING_LIST.length - 1) {
        setCurrentStep(currentStep + 1);
      } else {
        router.push('/login'); // 그냥 로그인 페이지로 보냄
      }
    };
    
    
    const onboarding = req.cookies.get('onboarding')?.value;
    
    if (onboarding === 'done' && isOnboardingPath) {
      // 근데 /dashboard 로 잘못 보내고 있었음 (버그)
      return NextResponse.redirect(new URL(token ? '/user/dashboard' : '/dashboard', req.url));
    }

     

    - 과거에 만든 미들웨어 코드입니다. 지금와서 보니 많이 아쉽습니다.. 

    - 일단 가장 큰 문제는 쿠키 형식으로 구현했었는데 문제는 DB에 저장을 하지 않아 유저가 개발자라면 쿠키에 데이터를 삭제시키면 문제가 생길거 같습니다. 

    - 또하 경로또한 팀원들끼리 정했던 규칙에서 바뀌는 상황이 발생해 이를 또한 수정이 필요해 보입니다. 

     

    개선점 찾아보기 

    1. 일단 가장 먼저 온보딩했다고 토큰이나 불린 값으로 DB에 저장하기 

    2. 미들웨어 경로 다시 수정해서 지정하기  

    🚨 여기서 주의할 점 
    1. 일단 가장 먼저 온보딩했다고 토큰이나 불린 값으로 DB에 저장하기  -> 2가지를 상황을 가정해야 합니다.
            a. 유저가 온보딩을 봤는데 로그인을 안했다면? 
            b. 유저가 로그인을 하지 않고 온보딩을 봤는데 로그인을 한다면 그때의 온보딩 처리 

    - 위와 같은 두가지 상황을 추가로 고려해서 생각해봤습니다.

    두가지 해결해보자

    // app/api/onboarding/complete/route.ts
    export async function POST() {
      const session = await getServerSession(authOptions);
    
      // 로그인 유저면 DB에 저장 (다른 경로)
      if (session?.user?.id) {
        await supabase
          .from('users')
          .update({ onboarding_completed: true })
          .eq('id', session.user.id);
    
        return NextResponse.json({ redirectTo: '/user/dashboard' });
      }
    
      // ✅ 비로그인이면 쿠키를 심는다
      const response = NextResponse.json({ redirectTo: '/demo' });
    
      response.cookies.set('onboarding', 'done', {
        httpOnly: true,        // JS에서 접근 불가 (보안)
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax',
        maxAge: 60 * 60 * 24 * 365, // 1년
        path: '/',
      });
    
      return response;
    }

     - 저는 다음과 같이 해결해봤습니다.

    - 로그인을 한 유저가 온보딩을 완료시 :  기존 수퍼베이스 users 테이블에 온보딩을 완료하면 onboarding_completed : true로 DB에 저장을 하고 리다이렉트를 시켰습니다. 

    - 로그인을 하지 않은 유저가 온보딩을 완료시 : 쿠키에 onboarding:done 이라는 값을 넣고 온보딩을 한번 보면 더 이상 접근을 하지 않도록 쿠키에 데이터를 넣었습니다.

     

    - 다음으로는 미들웨어 처리입니다.

    // middleware.ts
    export default async function middleware(req: NextRequest) {
      const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
      const onboardingCookie = req.cookies.get('onboarding')?.value;
    
      const isLoggedIn = !!token?.id;
      const hasGuestOnboardingCookie = onboardingCookie === 'done';
    
      // ✅ 핵심: 로그인 됨 + 쿠키 있음 → DB에 동기화
      if (isLoggedIn && hasGuestOnboardingCookie) {
        await syncGuestOnboardingToDb(token.id);
      }
    
      // ...이후 리다이렉트 로직
    }
    
    // libs/onboarding/onboarding.server.ts
    export async function syncGuestOnboardingToDb(userId: string) {
      const supabase = getSupabaseAdmin();
    
      await supabase
        .from('users')
        .update({
          onboarding_completed: true,
          updated_at: new Date().toISOString(),
        })
        .eq('id', userId);
    }
    
    그리고 동기화 후 쿠키는 응답에서 삭제합니다.
    
    // middleware.ts 의 respond 함수
    const respond = (response: NextResponse) => {
      if (isLoggedIn && hasGuestOnboardingCookie) {
        response.cookies.delete('onboarding'); // ✅ 쿠키 삭제
      }
      return response;
    };
    
    // middleware.ts
    // 로그인 됨 + 쿠키 있음 → DB에 동기화
    if (isLoggedIn && hasGuestOnboardingCookie) {
      await syncGuestOnboardingToDb(token.id); // ← 이 부분
    }
    
    // libs/onboarding/onboarding.server.ts
    export async function syncGuestOnboardingToDb(userId: string) {
      await supabase
        .from('users')
        .update({ onboarding_completed: true })
        .eq('id', userId);
    }
    
    // 과거
    export const config = {
      matcher: ['/', '/user/:path*', '/onboarding/:path*'],
    };
    
    // 현재
    export const config = {
      matcher: ['/', '/user/:path*', '/onboarding/:path*', '/login'],
    };

    - 미들웨어로 유저가 경로에 접근할려고 할때를 제어해봤습니다. 

    - 게스트 때 온보딩을 완료한 사용자가 나중에 로그인하면 미들웨어가 쿠키를 감지해서 DB에 자동으로 동기화하고 쿠키를 삭제합니다.

    미들웨어 ?? 
    - 미들웨어란 요청과 응답 사이에 끼어드는 코드라고 생각합니다. 
    - 이해는 되는데 이거를 왜 사용하는지는 예시를 봅시다. 
    미들웨어 없이 각 페이지에서 직접 처리하면 이렇게 됩니다.
    
    // 모든 protected 페이지마다 이 코드가 반복됨
    const { data: session } = useSession();
    
    useEffect(() => {
      if (!session) router.replace('/login');
    }, [session]);​
    - 아래 코드를 보면 세션으로 유저가 로그인했는지 파악하고 로그인이 안되어 있다면 UseEffect 안의 조건문이 실행되어 login으로 이동을 합니다. 
    - 여기서 하나의 문제가 발생하는데 우선 화면이 바로 이동하는게 아니라 리다이렉트가 일어나 화면이 나오고 그 다음 이동을 한다는 점입니다. 그리고 만약 위와 같이 다른 곳도 로그인이 되었는지 일일히 처리하기 위해서 위 코드를 반복적으로 호출해야 한다는 문제도 있습니다. 
    - 이때 미들웨어를 사용해서 처리하면 좋습니다. 
    - 하나 주의사항이 있는데 미들웨어는 기본적으로 matcher 경로를 따로 지정해주지 않으면 어떤 페이지에서든 미들웨어가 먼저 실행이 되어 성능의 저하가 생길수도 있습니다. 
    export const config = {
      matcher: ['/', '/user/:path*', '/onboarding/:path*', '/login'],
    };​
    - 그래서 다음과 같이 특정 미들웨어가 필요한 경로에만 다음과 같이 작성해주는 것이 올바른 방법입니다. 

     

    마무리

    작동은 했지만 구멍이 있던 코드를 다시 들여다보니 사실 당연히 고려해야 했던 상황들이었습니다. 이번 개선을 통해 온보딩 완료 여부를 DB와 쿠키 두 곳에서 관리하고 미들웨어 하나로 모든 경우를 제어하게 됐습니다. 다음엔 처음 설계할 때부터 이런 예외 상황을 먼저 생각해보려 합니다.

Designed by Tistory.