본문 바로가기

정규표현식 일치탐색 수행 시 놓치기 쉬운 이슈

사내 앱 환경은 UserAgent 문자열을 보고 앱에서 접근했는지 여부를 알 수 있다.

const UA_SAMPLE = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/605.1 APP_NAME(inapp; search; 1000; 11.5.5.2; 11)'

 

따라서 정규표현식으로 UserAgent를 분해해서 디바이스 환경 및 앱 환경 여부를 검사하는 parseDeviceInfo 함수를 선언했다.

const appUserAgentRegEx = /APP_NAME\([inapp]+;[^0-9]*(search);[^0-9]*(\d+);[^0-9]*(\d+.\d+.\d+).*\)/gim

const getAppInfo = (ua: string) => appUserAgentRegEx.exec(ua)

const parseDeviceInfo = (ua: string) => {
  const parser = new UAParser() // ua-parser-js
  parser.setUA(ua)
  const result = parser.getResult()
  
  const appMatchingData = getAppInfo(result.ua)
  const isApp = appMatchingData ? appMatchingData[1] === 'search' : false
  const appVersion = appMatchingData ? appMatchingData[3] : '0.0.0'
  
  return {
    ...result.device,
    ...result.os,
    isApp,
    appVersion
  }
}

 

 nextjs의 서버에서 위 함수를 호출하여 다음과 같이 디바이스 정보를 미리 가져온다.

_app.tsx 에서 가장 먼저 실행하였다.

 

const App = () => {}

App.getInitialProps = async (appContext: AppContext) => {
  const {ctx} = appContext
  
  const ua = isBrowser ? navigator.userAgent : ctx.req?.headers['user-agent'] || ''
  const deviceInfo = parseDeviceInfo(ua)
  
  return {
    deviceInfo
  }
}
{
  // ... 중략 ...
  isApp: true,
  appVersion: '11.7.1'
}

 

결과로는 제대로 받아오는 것을 확인하여 안심하고 있었으나 문제는 다른 페이지에서 또 디바이스 정보를 가져와야 할 경우에 생겼다.

const Page = () => {}

export const getServerSideProps = async (ctx) => {
  const ua = ctx.req.headers['user-agent'] || ''
  
  const {isApp} = parseDeviceInfo(ua)
  
  if (isApp) { // 앱에서 접속 시 바로 이동
    return {
      redirect: {
        destination: url,
        permanent: false
      }
    }
  }
  
  return {
    props: {
      url
    }
  }
}
{
  // ... 중략 ...
  isApp: false,
  appVersion: '0.0.0'
}

분명 같은 UserAgent인데 두번째 실행에서 다른 결과를 받았다.

 

getAppInfo 의 반환값이 null 로 반환되어 이슈를 찾아보니, RegExp.exec 에서 다음 부분을 내가 놓치고 있었다.

 

If your regular expression uses the "g" flag, you can use the exec() method multiple times to find successive matches in the same string. When you do so, the search starts at the substring of str specified by the regular expression's lastIndex (en-US) property (test() will also advance the lastIndex (en-US) property).

(출처)

 

g 플래그 사용 시 exec는 검사한 마지막 인덱스를 기억하고 있기 때문에 문자열의 연속적인 검사를 수행할 수 있다.

따라서 appUserAgentRegEx 에 g 플래그를 설정했기 때문에 두번째 실행에서 null을 받은 것이었다.

 

해결 방법으로는 g 플래그를 없애거나, lastIndex를 0으로 초기화해주면 된다.

const appUserAgentRegEx = /.../gim

const getAppInfo = (ua: string) => {
  const matchingData = appUserAgentRegEx.exec(ua)
  appUserAgentRegEx.lastIndex = 0
  return matchingData
}