ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Next.js - middleware 사용하기 (로그인 연동하기)
    React.js & Next.js 2022. 9. 25. 21:49

    이번 로그인 연결은 세션을 사용하기로 하였다. 웹 브라우저에서는 직접적으로 세션 ID 에 접근이 불가능하지만, 세션 쿠키에 들어있을 경우 자동으로 인식하여 헤더에 넣어 보내주기 때문에 로그인 로직을 구현하는데 문제가 되지 않았다.

    시나리오에 사용되는 페이지는 다음과 같다

    1. sign_up (회원가입 페이지)
    2. sign_in (로그인 페이지)
    3. welcome (로그인 시 접근하는 페이지)

    해당 페이지에 미들웨어를 연동하여, 다음 상황을 구현할 것이다

    1. 로그인한 이후 /sign_in 페이지에 접근할 경우, /welcome 페이지로 보내주기
    2. 로그인하지 않은 경우 /welcome 페이지에 접근할 경우, /sign_in 페이지로 보내주기

    따라서 미들웨어에서는 각각의 페이지에 대한 정보를 먼저 받아야 하고, 이를 미들웨어에 정의한 메서드에게 전달해줘야 한다. 이전에 middlewarematcher 에 대해서 다뤘기 때문에 미들웨어 내부 코드에 대한 설명은 간략하게 적도록 한다.

    import { withAuth, withoutAuth } from 'middlewares/auth.middleware'
    import { NextRequest, NextResponse } from 'next/server'
    
    export async function middleware(request: NextRequest) {
      if (request.nextUrl.pathname.startsWith('/sign_up')) {
        console.log('call middleware - /sign_up')
    
        return await withoutAuth(request)
      }
    
      if (request.nextUrl.pathname.startsWith('/sign_in')) {
        console.log('call middleware - /sign_in')
    
        return await withoutAuth(request)
      }
    
      if (request.nextUrl.pathname.startsWith('/welcome')) {
        console.log('call middleware - /welcome')
    
        return await withAuth(request)
      }
    }
    
    export const config = {
      matcher: [
        '/sign_up/:path*',
        '/sign_in/:path*',
        '/welcome/:path*',
      ],
    }
    if(request.nextUrl.pathname.startsWith('/sign_up')){
    
    }

    /sign_up 으로 pathname이 시작되는 페이지의 경우에 어떠한 이벤트를 시작하겠다는 미들웨어의 pathname의 캐치하는 기본적인 동작 원리이다.

    정상적으로 호출되는지 확인하기 위해 console.log() 를 지속적으로 찍어둔다. 해당 로그는 middleware에 의해 동작하므로 브라우저에서 보는 것이 아닌 터미널에서 관측할 수 있다.

    /welcome 페이지에 접근할 경우

    Advanced Features: Middleware | Next.js

    [Advanced Features: Middleware | Next.js

    Learn how to use Middleware to run code before a request is completed.

    nextjs.org](https://nextjs.org/docs/advanced-features/middleware)

    사용한 커스텀 미들웨어 메서드는 두 가지이다.

    • withAuth - session이 없는 경우에 Auth(인증)이 필요한 경우를 나타내는 메서드
    • withoutAuth - session이 이미 전달되었고, Auth(인증)이 이미 완료되어 필요 없는 경우에 사용하는 메서드

    fetch 메서드 사용 (기본 POST 메서드 예제)

    [Fetch 사용하기 - Web API | MDN

    Fetch API는 HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스를 제공합니다. Fetch API가 제공하는 전역 fetch() (en-US) 메서드로 네트워크의 리소

    developer.mozilla.org](https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch)

    // POST 메서드 구현 예제
    async function postData(url = '', data = {}) {
      // 옵션 기본 값은 *로 강조
      const response = await fetch(url, {
        method: 'POST', // *GET, POST, PUT, DELETE 등
        mode: 'cors', // no-cors, *cors, same-origin
        cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
        credentials: 'same-origin', // include, *same-origin, omit
        headers: {
          'Content-Type': 'application/json',
          // 'Content-Type': 'application/x-www-form-urlencoded',
        },
        redirect: 'follow', // manual, *follow, error
        referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
        body: JSON.stringify(data), // body의 데이터 유형은 반드시 "Content-Type" 헤더와 일치해야 함
      });
      return response.json(); // JSON 응답을 네이티브 JavaScript 객체로 파싱
    }
    
    postData('https://example.com/answer', { answer: 42 }).then((data) => {
      console.log(data); // JSON 데이터가 `data.json()` 호출에 의해 파싱됨
    });

    공통 데이터 패칭 메서드 - fetch

    function validateUser(req: NextRequest): Promise<Response> {
      return fetch('특정라우팅 경로', {
        method: 'GET',
        mode: 'cors',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
          cookie: `세션 쿠키 ID=${req.cookies.get('세션 쿠키 ID')}`,
        },
      })
    }

    이번 로그인 흐름에는 세션을 사용하기 때문에, header 조건이 다를 수 있다. 각 백엔드와의 로직에 맞춰 header를 설정해주면 된다.

    기존에는 axios를 사용했는데, 아직까지 미들웨어를 사용할 때 axios의 기본 설정만으로는 ajax 요청을 보낼 수 없는 상황이다.

    [TypeError] adapter is not a function

    다음과 같은 상황을 마주할 수 있다. 아직 기본적인 설정으로는 해결이 불가능하고 adapter 설정을 위한 추가 라이브러리를 사용해야 하므로 안정화될 때 까지는 fetch 메서드를 사용하는 편이 좋을 것 같다.

    해당 이슈를 핸들링할 수 있는 URL 을 첨부한다.

    【情報求む】next12のmiddlewareでTypeError adapter is not functionが出た場合の対処法

    [【情報求む】next12のmiddlewareでTypeError adapter is not functionが出た場合の対処法

    バッジを贈って著者を応援しよう バッジを受け取った著者にはZennから現金やAmazonギフト券が還元されます。 バッジを贈る

    zenn.dev](https://zenn.dev/nicopin/articles/5dc87c27bd08de)

    withAuth

    export async function withAuth(req: NextRequest) {
      try {
        const url = req.nextUrl.clone()
        url.pathname = '/sign_in'
    
        const response = await validateUser(req)
    
        if (response.status === 200) return NextResponse.next()
        if (response.status === 401) return NextResponse.redirect(url)
      } catch (error) {
        console.log('err: ', error)
        throw new Error(`Couldn't check authentication`)
      }
    }

    req.nextUrl 프로퍼티가 가지고 있는 clone 메서드를 사용하여 값을 복사한 뒤 url에 담는다. 이후 url에 가고자 하는 pathname 을 재할당한다.

    기본적으로 validateUser 함수를 호출한 뒤의 내부의 정보(res.json())가 필요한 것이 아닌, 요청이 정상적으로 전달되어 결과값이 있는 경우 세션 ID가 있다는 의미이므로 해당 경우에는 next 메서드를 호출하여 이후 작업을 정상적으로 처리할 수 있도록 한다.

    만약 401 과 같이 권한이 없는 경우에는 로그인이 필요한 상황인 경우이므로 /sign_in 페이지로 리다이렉트 한다. 따라서 세션 ID가 없는 사용자의 경우 절대 /welcome 페이지를 만날 수 없다.

    withoutAuth

    export async function withoutAuth(req: NextRequest) {
      try {
        const url = req.nextUrl.clone()
        url.pathname = '/welcome'
    
        const response = await validateUser(req)
    
        if (response.status === 200) return NextResponse.redirect(url)
        if (response.status === 401) return NextResponse.next()
      } catch (error) {
        console.log('err: ', error)
        throw new Error(`Couldn't check authentication`)
      }
    }

    req.nextUrl 프로퍼티가 가지고 있는 clone 메서드를 사용하여 값을 복사한 뒤 url에 담는다. 이후 url에 가고자 하는 pathname 을 재할당한다.

    /sign_in 페이지에 접근했을 때, pulseData 함수를 호출하여 200 일 경우 세션 ID 가 있는 경우이므로 /sign_in 페이지에 있을 필요가 없으므로 /welcome 페이지로 리다이렉트 해준다.

    /sign_up 페이지의 경우 다른 방식의 회원가입이 필요할 수 있지만, 연습을 위해 같이 withoutAuth 메서드로 처리하였다.

    /useLogin

    import { useState } from 'react'
    
    const useLogin = () => {
      const [info, setInfo] = useState({
        데이터
      })
    
      return { info, setInfo }
    }
    
    export default useLogin

    두 페이지에서 같은 useState를 사용하기 때문에 훅으로 묶어줬다.

    /sign_up

    import { AxiosResponse } from 'axios'
    import { defaultAxios } from 'config/axiosConfig'
    import useLogin from 'hook/useLogin'
    import { useRouter } from 'next/router'
    import React, { useCallback, useState } from 'react'
    
    const SignUp = () => {
      const { info, setInfo } = useLogin()
      const [message, setMessage] = useState('')
      const router = useRouter()
    
      const onSubmit = useCallback(
        async (e: React.FormEvent) => {
          e.preventDefault()
          setMessage('')
    
          const data = {
            user: {
              유저 정보가 들어있는 데이터
            },
            profile: {
              프로필 정보가 들어있는 데이터
            },
          }
    
          await defaultAxios
            .post('/라우팅_경로', data)
            .then((res: AxiosResponse) => {
              console.log('res:', res)
              if (res.status === 응답 상태코드) router.push('/sign_in')
            })
            .catch((err) => {
              console.error('- SignUp: error occured in OnSubmit', err)
    
              if (err.response.status === 에러 상태코드) return setMessage(err.response.data?.message)
    
              if (err.response.status === 에러 상태코드) return setMessage(err.response.data.message)
    
              return setMessage('Oops, something went wrong. You need to check your server')
            })
        },
        [info, router]
      )
      return (
        <S_Auth onSubmit={onSubmit}>
          <div>
            <label>ID</label>
            <input
              type="email"
              value={info.email}
              onChange={(e) => setInfo({ ...info, email: e.target.value })}
              placeholder="enter your email"
            />
          </div>
    
          <div>
            <label>PW</label>
            <input
              type="password"
              value={info.password}
              onChange={(e) => setInfo({ ...info, password: e.target.value })}
              placeholder="enter your password"
            />
          </div>
    
          <div>
            <label>last name</label>
            <input
              type="text"
              value={info.last_name}
              onChange={(e) => setInfo({ ...info, last_name: e.target.value })}
              placeholder="enter your last name"
            />
          </div>
    
          <div>
            <label>first name</label>
            <input
              type="text"
              value={info.first_name}
              onChange={(e) => setInfo({ ...info, first_name: e.target.value })}
              placeholder="enter your first name"
            />
          </div>
    
          <div>
            <button type="submit">sign up</button>
          </div>
    
          {message && <p className="msg">{message}</p>}
        </S_Auth>
      )
    }
    
    export default SignUp

    /sign_up 의 경우 일반 로그인 로직과 다르지 않다. 백엔드와 협의한 response.status , error.response.status 에 대한 처리를 해준다.

    /sign_in

    import { AxiosResponse } from 'axios'
    import { defaultAxios } from 'config/axiosConfig'
    import useLogin from 'hook/useLogin'
    import { useRouter } from 'next/router'
    import React, { useCallback, useState } from 'react'
    
    const SignIn = () => {
      const { info, setInfo } = useLogin()
      const [message, setMessage] = useState('')
    
      const router = useRouter()
    
      const onSubmit = useCallback(
        async (e: React.FormEvent) => {
          e.preventDefault()
    
          const data = {
            user: {
              유저 데이터
            },
          }
    
          await defaultAxios
            .post('/라우팅_경로', data)
            .then((res: AxiosResponse) => {
              console.log('res:', res)
              if (res.status === 응답 상태코드) router.reload()
            })
            .catch((err) => {
              console.error('- SignIn: error occured in OnSubmit', err)
    
              if (err.response.status === 에러 상태코드) return setMessage(err.response.data.message)
    
              return setMessage('Oops, something went wrong. You need to check your server')
            })
        },
        [info.email, info.password, router]
      )
    
      return (
        <>
          <S_SignIn onSubmit={onSubmit}>
            <div>
              <label>ID</label>
              <input
                type="email"
                value={info.email}
                onChange={(e) => setInfo({ ...info, email: e.target.value })}
                placeholder="enter your email"
                required
              />
            </div>
    
            <div>
              <label>PW</label>
              <input
                type="password"
                value={info.password}
                onChange={(e) => setInfo({ ...info, password: e.target.value })}
                placeholder="enter your password"
                required
              />
            </div>
    
            <div>
              <button type="submit">login</button>
              <button type="button" onClick={() => router.push('/sign_up')}>
                sign up
              </button>
            </div>
    
            {message && <p className="msg">{message}</p>}
          </S_SignIn>
        </>
      )
    }
    
    export default SignIn

    여기서 주목해야할 점은 axios 요청 성공 이후에 대한 핸들링 코드이다.

    await defaultAxios
        .post('/라우팅_경로', data)
        .then((res: AxiosResponse) => {
          console.log('res:', res)
          if (res.status === 응답 상태코드) router.reload()
        })

    next의 useRouter가 제공하는 router.reload() 메서드를 통해 해당 페이지를 리로드하게 된다. 로그인이 성공한 이후에는 세션 ID가 세션 쿠키에 전달된 이후이기 때문에 해당 /sign_in 페이지를 리로드할 경우, 미들웨어에 의해 welcome 페이지로 보내지게 된다.

    위에 첨부한 middleware.ts 에 대한 특정 시나리오( /sign_in )이다

    import { withAuth, withoutAuth } from 'middlewares/auth.middleware'
    import { NextRequest, NextResponse } from 'next/server'
    
    export async function middleware(request: NextRequest) {
      ...
    
      if (request.nextUrl.pathname.startsWith('/sign_in')) {
        console.log('call middleware - /sign_in')
    
        return await withoutAuth(request)
      }
        ...
    }
    
    export const config = {
      matcher: [
        ...,
        '/sign_up/:path*',
            ...,
      ],
    }
    export async function withoutAuth(req: NextRequest) {
      try {
        const url = req.nextUrl.clone()
        url.pathname = '/welcome'
    
        const response = await validateUser(req)
    
        if (response.status === 200) return NextResponse.redirect(url)
        if (response.status === 401) return NextResponse.next()
      } catch (error) {
        console.log('err: ', error)
        throw new Error(`Couldn't check authentication`)
      }
    }

    결과 보기

    1. 로그인

    로그인 성공 시, reload 이벤트를 걸어놨기 때문에 세션 쿠키가 웹 브라우저에 저장된 상태로 sign_in 페이지에 다시 접근하게 된다. 하지만, 미들웨어에 의해 validateUser 메서드를 통해 유효성 검사를 했기 때문에 세션 쿠키가 정상적으로 들어있음을 확인하고 welcome 페이지로 라우팅하게 된다

    2. 다른 경로 이동

    로그인 성공 시, sign_in 페이지에 접근할 필요가 없다. 이미 유저에 대한 정보가 세션 쿠키에 저장되어 있기 때문이다. 따라서 미들웨어의 커스텀 메서드를 통해 sign_in 에 접근할 경우 다시 welcome 페이지로 리다이렉트 해주고 있다.

    3.세션 쿠키 삭제 시

    세션 쿠키를 삭제할 경우 더 이상 사용자의 상태를 검증할 유효한 세션 쿠키가 없기 때문에, welcome 페이지에 접근하더라도 sign_in 페이지로 리다이렉트 된다.

    댓글

Designed by Tistory.