strapi v4 → v5 identifier 처리

2025. 07. 28.

배경

Strapi가 v5 로 업데이트되면서 많은 breaking changes가 생겼는데, 그 중 서비스 운영에 직접적으로 영향을 끼치는 부분 중 하나가 API 요청에서 id 대신 documentId를 사용해야 한다는 변경점이었다. 다른 변경점들은 일반 사용자들은 눈치채지 못할만한 변경점들이었지만, 이 변경점은 일반 사용자들에게도 영향을 끼치는 변경점이었다. 이 문제를 Rumyscape에서 해결한 경험에 대해 기록을 남겨본다.

왜 Strapi는 기존 id를 사용하지 않게 했을까?

기존 Strapi v4 에서는 id 라는 키에 (별도로 커스텀하지 않는다면) sequential 한 값을 넣어주고 있었다. 이는 짧고 명확한 id 라는 장점을 가지고 있었지만, 반대로 보안 측면에서는 취약했다. (공격자들이 resource 의 id 를 쉽게 추론 가능하므로 - 참고)

추가로 하나의 entry가 여러 버전이나 localizations을 가질 수 있지만 id는 하나라는 불안정성과, duplication이나 import/export 작업에서 바뀌는 문제가 있을 수 있었다고 한다. (공식 문서 발췌 - 참고)

이런 한계를 해결하기 위해서, Strapi v5에서는 documentId라는 random-generated 필드가 만들어졌다.

문제가 될 수 있는 시나리오

개발자의 관점에서 migration이 귀찮다는 문제를 제외하고는 사용성이 높아지는 좋은 변경점이라고 생각했다. 그럼에도 불구하고 Strapi를 활용해 서비스를 운영하는 입장에서는 몇몇 문제가 될 수 있는 상황들이 있었다.

  1. 기존 URL효화된다.
    • https://www.rumyscape.com/cafes/1 이라는 링크가 존재했다고 가정하자. 서버가 Strapi v5 로 업데이트 하기 전에 URL공유했던 사용자들은 업데이트 이후 링크를 접속하려고 하면 존재하지 않는 링크가 되는 이슈가 발생한다.
  2. SEO에 치명적인 이슈가 생긴다.
    • 구글 등 검색엔진들이 이미 서비스의 링크를 긁어간 상태인데, 업데이트 이후에 검색엔진에서 다시 모든 페이지를 업데이트 하기 전까지 검색엔진에서 진입할 때도 존재하지 않는 링크로 도달할 것이다.

결국 UX적인 문제와 서비스 운영의 신뢰도에 문제가 생기는 크리티컬한 이슈가 된다.

그렇다면 어떻게 해결할 수 있을까

내가 생각한 방식은 아래와 같다.

(아래 방법은 기존 Database를 그대로 migration 해서 id (lagacy)가 동일한 상태로 documentId 만 새로 생겼다는 전제를 바탕으로 한다.)

  1. 기존 id 를 documentId 로 전환한 값을 응답하는 /api/<collection-name>/id-to-documentId/:id 라는 custom endpoint 를 만든다.
  2. 클라이언트(e.g. Next.js)에서 상세 페이지를 로딩할 때 parameter 가 integer 타입이라면 1번에서 작성한 API 로 요청을 보내고, 응답받은 documentId를 Location으로 하는 308 Permanent redirect 를 응답한다.

이렇게 하면 유저들도 서비스 이용에 불편함이 없고, 308 Permanent redirect 의 경우 검색엔진의 결과에서 기존 페이지를 대체하고 싶을 때 사용하라고 권장되기 때문에 (구글 개발자 문서 - 참고) SEO 관점에서도 최적의 해결 방법이라고 생각했다.

하지만 Custom API 를 작성할 때 문제가 발생했다. Strapi v5에서 제공하는 Document Service API를 통해서는 legacy id를 기준으로 데이터를 가져오는 방법이 존재하지 않았다.


Strapi v4 에서는 코드를 통해 데이터를 가져오는 방법으로 Entity Service API 와 Query Engine API 두 개를 지원했다.

  • Entity Service API
    • High level API. 즉, Strapi 가 가이드하는 방법대로 사용하기 쉽게 만들어진 API.
    • Strapi v5 에서 Document Service API 로 대체됨.
  • Query Engine API
    • Low lever API. Plugin 개발자들이나 커스텀 비즈니스 로직을 작성할 때 사용하는 용도.
    • Strapi v5 에서는 사라짐.

내가 맨 처음 생각한 방식은 Query Engine API 의 사용을 염두에 둔 방법이었는데, 이렇게 되면 문제를 해결할 수가 없을 것 같았다.

하지만 다행히도 Strapi의 코드를 확인해 보니, 아직 코드들은 살아있었다.

strapi-v5-entity-service Strapi v5 Entity Service의 코드 - deprecated 처리가 되어있다.

strapi-v5-query-engine Strapi v5 Query Engine 의 코드 - deprecated 처리가 되어있지 않다.

코드 중 공식문서상에서는 삭제된 Query Engine API 부분은 Entity Service API 와는 다르게 deprecated 되지 않았었다. 실제로 v4 → v5 breaking changes 에도 Query Engine API 의 대체에 대한 설명은 없다.

공식 문서상에서 사라진 방법이지만, 지금 문제를 해결할 수 있는 내가 찾은 유일한 방법이기에 (그리고 v5 의 변경점에 포함되지 않은 부분이기에 당분간 삭제되지 않을 것이라는 확신을 가지고) Query Engine API 를 활용해 id-to-documentId API 를 작성할 수 있었다.

// controller 파일의 일부
async idToDocumentId(ctx) {
  const id = ctx.params.id;
  const cafe = await strapi.db
    .query('api::<api-name>.<content-type-name>')  // 실제로는 각각의 이름이 들어갔다
    .findOne({ where: { id } });

  return {
    data: {
      documentId: cafe.documentId,
    },
  };
},

마무리하며

  1. 첫 도입 당시 개발 속도를 위해 low-level framework (e.g. NestJS) 가 아닌 high-level framework 인 Strapi 를 도입했는데, 이런 업데이트 등에서는 오히려 추가로 대응할 것이 생기곤 한다. 어쩔 수 없는 trade-off 인 것 같다.
  2. Strapi 의 v5 업데이트 중 이해가 가지 않는 부분이, 실제로 삭제되지 않았고 (deprecated 조차도 되지 않았고) codemod 의 수정에도 포함되지 않은 Query Engine API 가 왜 공식문서에서만 삭제되었는지 모르겠다.