json-server 뜯어고쳐 사용하기

Moon Ki, Kim·2024년 4월 12일
7
post-thumbnail

json-server 패키지를 뜯어고쳐 원하는 기능을 추가해 사용해보자!

필요성

백엔드가 아직 작업이 덜 되었거나, 따로 백엔드 서버를 만들지 않고 사용하고 싶을때
json-server를 주로 사용하는데, 이는 local에 저장해놓은 JSON 파일을 db"처럼" 사용하여
서버와의 http통신을 할 수 있게 도와준다.

그러나, json-server에서는 자체적으로 api에 대한 Filter를 적용하기 위한 규칙이 존재하여서
실제 api 문서와 동일하게 처리할 수 없다는 문제점이 있다.
예를 들어 (구현이 곧 될) 백엔드에서는 GET을 통해 api 호출을 할 때 startTime필드와 endTime을 query로 넘겨 그 사이값을 필터링 할 수 있다고 하면

axios
  .get(
  `${process.env.REACT_APP_API_URL}/data?startTime=${startTime}&endTime=${endTime}`)
  .then(
  ...
);

위와 같이 보내야 하지만,

json-server의 공식 문서를 살펴보게 되면 params를 비교할 때 conditions를 지정할 수 는 있지만, 위와 다르게 코드를 적어야 한다.
axios
  .get(
  `${process.env.REACT_APP_API_URL}/data?startTime_gte=${startTime}&endTime_lte=${endTime}`)
  .then(
  ...
);

이렇게 같은 api가 다른 명세를 가지게 된다면 mock서버로 미리 테스트를 하는 의미가 퇴색된다(고 생각한다)

그래서 json-server를 커스터마이징하여 사용해보기로 했다.

Before content

해당 글에서는 trouble shooting 과정에서 사용한 코드와 데이터가 아닌, 포스팅을 위해 따로 작성한 코드와 데이터를 사용한다.
데이터 구조는 다음과 같은 interface를 가진다.

interface responseData {
  _id: string;
  startTime: string;
  endTime: string;
  data: innerData[];
}

interface innerData{
  value: number;
  test: string;
}

실제 데이터는 아래처럼 들어온다고 가정하자
이때 시간 데이터는 ISO의 포멧을 따르며, 맨 뒤의 microsecond는 있을수도있고, 없을수도있다.
velog/velog.json

{
  "test1": [
    {
      "_id": "660fab3f87d9d6472afa89c0",
      "startTime": "2024-02-01T09:28:32.020000",
      "endTime": "2024-02-01T09:45:29", 
      "data": [
        {
          "value": 1,
          "test": "string"
        }
      ]
    },
    ...
    ]
}

Trouble shooting

console.log 찍기

그러나, 시작부터 문제점에 부딪혔다

date의 filter기능을 추가하기 위해 op의 값으로 switch문이 시작되기 전에 console.log를 통해 itemValue, paramValue값이 제대로 들어오는지 확인해보고자 했다.

위의 velog/velog.json파일을 db삼아 json-server를 실행시키기 위해 아래와 같은 명령어를 cmd에 입력한다

// json-server {json파일 위치} --port {portNumber}
json-server ./velog/velog.json --port 8080

그러나 chrome dev tools의 콘솔창에서도, terminal의 콘솔창 그 어느곳에서도 console.log의 결과값을 찾을 수 없었다.

해당 상황에 대해 구글링을 해봤을 때 2015년의 git issue를 하나 찾을 수 있었다.

(아무튼 json-server말고 node로 실행하면 된다는 내용)

위 git issue에서는 node.js로 만들어진 프로젝트(json-server의 v0 브랜치를 보면 확인할 수 있다)여서 그랬는지 node server.js를 통해 실행했지만 지금의 json-server는 react로 생성된 프로젝트여서 server.js가 없다.

하지만, package.json을 보면

scripts의 dev에서 tsx(TypeScript Execute)를 통해 src/bin.ts 실행시키고, fixtures/db.json을 사용한다는것을 확인할 수 있다.
tsx를 npx를 통해 설치하기는 귀찮으니 package.json을 아래와 같이 바꿔줬다

Fixtures/db.json파일을 사용하던 것을 내가 사용하길 원하는 velog/velog.json으로 바꿔주면서 bin.ts파일을 열어보니 아래 사진처럼 port정보 또한 args로 받고있길래

내친김에 port까지 8080번을 사용하도록 설정했다.

이후 npm run dev를 통해 다시 파일을 실행시키면

tsx를 통해 파일이 실행되는것을 확인할 수 있고,

요청을 보내면 콘솔이 찍히는것 까지 확인할 수 있다.

Date filter 추가하기

service.ts를 통해 확인한 기존의 코드는 Date type에 비교를 지원하지 않는다.

위의 console.log를 찍은곳 아래의 op에 대한 switch문 안을 보면

// item_lte=value
case Condition.lte: {
  if (
    !(
      typeof itemValue === 'number' &&
      itemValue <= parseInt(paramValue)
    )
  ) {
    return false
  }
  break
}
...
... other conditions ...
...
// item=value
case Condition.default: {
  if (!(itemValue == paramValue)) return false
}

와 같이 itemValue (전체 db에서 searchParams의 값을 가지는 것)의 형식이 number인 경우에만
itemValue와 paramValue의 비교를 지원해주고, 아닌 경우에는 비교하지 않는다.
즉, 대소비교 operation을 통해 날짜에 대한 필터를 걸 수 없다.

또한, 내 목표는 startTime_gte=...와 같이 전후비교를 하는것이 아닌,
startTime=...&endTime=...와 같이 params를 걸고, 해당 기간 후의 일정을 가져오는것이 목표이다.
(해당 포스트에서는 startTime=...에 대한 필터링 기능만 작성할 예정이다 이유는 아래에...)

그러한 이유에서 Condition.default:에 필터링 기능을 걸어주는 것이 적당하다고 할 수 있겠다.

Date인지 확인하기

Condition.default:에 무작정 전후비교 기능을 넣어버리면 ~~/test1?id=660fae...와 같은 query를 보낼 수 없게된다.

그렇기 때문에 만약 query의 비교값이 Date형식인 경우에만 전후비교를 하기로 했다.

js의 그지같은 특징으로 Date('123456')는 Date로 인식하기 때문에 함수를 직접 짜보려고 했지만, 생각이 나질 않았다.

구글링을 통해 해결하고자 하였으나, 찾은 여러 해법 (isNaN + getDate()의 조합, new Date + getTime()의 조합...)은 모두 실패했다.
전 버전의 chromium 엔진을 사용하는 Chrome이나 firefox에서는 가능한 조합도 있었지만,
내가 사용하는 버전에서는 지원되지 않는듯 했다.

그래서 Date에 대한 정규식을 만들어서 엄격한 함수(robust function)를 만들어야 겠다고 생각했다.


이에 string type의 dateString을 매개변수로 받는 isValidDate함수를 선언했다.

function isValidDate(dateString: string) {}

Date는 ISO형식을 따르고, microsecond 이하의 범위는 있을수도 없을수도 있기 때문에 아래와 같이 정규식을 작성했다.

// Date 예시 : 2024-02-01T09:28:32.020000
const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3,6})?$/

정규식을 통해 startTime=123123과 같은 입력이 들어와도 regex.test(dateString)에서 거를 수 있도록 했다.

if (!regex.test(dateString)) {
  return false
}

이후 date형식에 맞다고 해도 (그럴리는 없겠지만) 유효한 date가 아닌 경우를 걸러주고자 간단한 검사를 넣어두었다.

const date = new Date(dateString)

  const isoDate = date.toISOString()
  const reformatDate =
    isoDate.slice(0, 11) +
    (Number(isoDate.slice(11, 13)) + 9).toString().padStart(2, '0') +
    isoDate.slice(13, 19)

  return reformatDate === dateString.slice(0, 19)

매개변수로 넘어온 string을 Date객체로 생성하고, ISO형식으로 변환한다.
해당 과정에서 한국은 UTC+9로 9시간 차이가 나기 때문에 적절한 부분에서 parsing하고,
9시간을 더해주어 formatting해주었고, return부분에서 microsecond부분을 제외한
나머지 파트를 비교하여 같은 경우 Date로 판단하기로 했다.

함수에 대한 검증 결과는 아래와 같다

/test1?startTime=123123 -> false
/test1?startTime=2024-02-27T14:29:22 -> true
/test1?startTime=2024-02-01T09:28:32.020000 -> true
/test1?startTime=2024-02-31T09:28:32.020000 -> false (2월 31일)
/test1?startTime=2024-02-00T09:28:32.020000 -> false (error)
/test1?startTime=2024-02-60T09:28:32.020000 -> false (error)

이 과정에서 00일이나 60일에 대해 Date로 감싸고 toISOString함수를 실행할 때 error가 발생했다. 이에 try-catch를 통해 error handling을 처리해주었고 최종 함수는 다음과 같다.

function isValidDate(dateString: string) {
  const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3,6})?$/
  if (!regex.test(dateString)) {
    return false
  }

  const date = new Date(dateString)

  try {
    const isoDate = date.toISOString()
    const reformatDate =
      isoDate.slice(0, 11) +
      (Number(isoDate.slice(11, 13)) + 9).toString().padStart(2, '0') +
      isoDate.slice(13, 19)

    return reformatDate === dateString.slice(0, 19)
  } catch (err) {
    return false
  }
}

Date filter 걸어주기

위의 isValidDate를 통해 해당 string이 Date type임이 확실해졌으니 이후는 간단하다
requested Query에서 key값을 가져와서 해당 key값이 startTime인 경우 이후의 값을 return 하고,
endTime인 경우 이전의 값을 return 하면 된다.

이는 Date.getTime() 메소드의 대소비교를 통해 쉽게 작성할 수 있었다.

// item=value
case Condition.default: {
  if (isValidDate(paramValue)) {
    console.log(new Date(paramValue), key)
    if (key === 'startTime') {
      return (
        new Date(paramValue).getTime() <
        new Date(itemValue as Date).getTime()
      )
    } else if (key === 'endTime') {
      return (
        new Date(paramValue).getTime() >
        new Date(itemValue as Date).getTime()
      )
    }
  }
  if (!(itemValue == paramValue)) return false
}

구현 결과

/test1

/test1?startTime=2024-09-00T09:00:00

/test1?endTime=2024-09-09T09:00:00

test1?startTime=2024-09-00T09:00:00


아쉬운점

위에서 (해당 포스트에서는 startTime=...에 대한 필터링 기능만 작성할 예정이다)와 같이 작성했었는데, multiple query option에 대해서 시도를 했지만 아직 해결하지 못한 상태이다.
나 전에도 여러 시도가 있었으나, (git issue) 아직은 해당 기능에 대한 update나 해결책이 제시되지는 않은 것 같다.
한번 구현을 해볼까 하였으나, service.ts의 구조를 뒤엎어야할 것만 같아서 지금 당장은 어렵고, 시간여유가 된다면 json-server에 contribute하는 식으로 해봐야겠다(라고 생각만 했다)

덧붙이는말

혹시 잘못된 정보가 있거나, 추가할 부분이 있으면 꼭! 댓글로 알려주세요 잘못된 정보를 공유하고 싶지 않아요

0개의 댓글