본문으로 건너뛰기

기본적으로 TanStack Router는 JSON.stringifyJSON.parse를 사용해 URL 검색 매개변수를 자동으로 파싱하고 직렬화합니다. 이 과정에는 검색 객체의 직렬화 및 역직렬화뿐 아니라 URL 검색 매개변수에서 일반적으로 사용하는 검색 문자열의 이스케이프 및 이스케이프 해제가 포함됩니다.

예를 들어 기본 구성을 사용하고 다음 검색 객체가 있다면:

const search = {
page: 1,
sort: 'asc',
filters: { author: 'tanner', min_words: 800 },
}

다음 검색 문자열로 직렬화되고 이스케이프됩니다.

?page=1&sort=asc&filters=%7B%22author%22%3A%22tanner%22%2C%22min_words%22%3A800%7D

다음 코드로 기본 동작을 구현할 수 있습니다.

React

import {
createRouter,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'

const router = createRouter({
// ...
parseSearch: parseSearchWith(JSON.parse),
stringifySearch: stringifySearchWith(JSON.stringify),
})

Solid

import {
createRouter,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'

const router = createRouter({
// ...
parseSearch: parseSearchWith(JSON.parse),
stringifySearch: stringifySearchWith(JSON.stringify),
})

하지만 이 기본 동작이 모든 사용 사례에 적합하지는 않을 수 있습니다. 예를 들어 base64 인코딩과 같은 다른 직렬화 형식을 사용하거나, query-string, JSURL2, Zipson처럼 직렬화/역직렬화 전용 라이브러리를 사용하려 할 수 있습니다.

Router 구성의 parseSearchstringifySearch 옵션에 직접 직렬화 및 역직렬화 함수를 제공하면 됩니다. 이때 TanStack Router에 내장된 헬퍼 함수인 parseSearchWithstringifySearchWith를 활용해 과정을 단순화할 수 있습니다.

[!TIP] 직렬화 및 역직렬화에서 중요한 점은 역직렬화 후 동일한 객체를 다시 얻을 수 있어야 한다는 것입니다. 직렬화 및 역직렬화 과정이 올바르게 수행되지 않으면 일부 정보가 손실될 수 있으므로 중요합니다. 예를 들어 중첩 객체를 지원하지 않는 라이브러리를 사용하면 검색 문자열을 역직렬화할 때 중첩 객체가 손실될 수 있습니다.

URL 검색 매개변수 직렬화 및 역직렬화의 멱등성을 보여주는 다이어그램

다음은 TanStack Router에서 검색 매개변수 직렬화를 사용자 지정하는 방법의 예시입니다.

Base64 사용

브라우저와 URL 언펄러 등에서 최대한 호환되도록 검색 매개변수를 base64로 인코딩하는 것이 일반적입니다. 다음 코드로 이를 수행할 수 있습니다.

React

import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'

const router = createRouter({
parseSearch: parseSearchWith((value) => JSON.parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(JSON.stringify(value)),
),
})

function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}

function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}

Solid

import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'

const router = createRouter({
parseSearch: parseSearchWith((value) => JSON.parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(JSON.stringify(value)),
),
})

function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}

function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}

⚠️ 이 스니펫에서 atob/btoa를 사용하지 않는 이유는 무엇인가요?

따라서 이 구성을 사용해 앞의 객체를 검색 문자열로 변환하면 다음과 같습니다.

?page=1&sort=asc&filters=eyJhdXRob3IiOiJ0YW5uZXIiLCJtaW5fd29yZHMiOjgwMH0%3D

[!WARNING] 사용자 입력을 Base64로 직렬화하면 URL 역직렬화와 충돌할 위험이 있습니다. URL이 올바르게 파싱되지 않거나 다른 값으로 해석되는 등 예기치 않은 동작이 발생할 수 있습니다. 이를 방지하려면 안전한 바이너리 인코딩/디코딩 방법을 사용해 검색 매개변수를 인코딩해야 합니다(아래 참고).

query-string 라이브러리 사용

query-string 라이브러리는 쿼리 문자열을 안정적으로 파싱하고 문자열화할 수 있어 널리 사용됩니다. 이 라이브러리를 사용해 검색 매개변수의 직렬화 형식을 사용자 지정할 수 있습니다. 다음 코드로 이를 수행할 수 있습니다.

React

import { createRouter } from '@tanstack/react-router'
import qs from 'query-string'

const router = createRouter({
// ...
stringifySearch: stringifySearchWith((value) =>
qs.stringify(value, {
// ...options
}),
),
parseSearch: parseSearchWith((value) =>
qs.parse(value, {
// ...options
}),
),
})

Solid

import { createRouter } from '@tanstack/solid-router'
import qs from 'query-string'

const router = createRouter({
// ...
stringifySearch: stringifySearchWith((value) =>
qs.stringify(value, {
// ...options
}),
),
parseSearch: parseSearchWith((value) =>
qs.parse(value, {
// ...options
}),
),
})

따라서 이 구성을 사용해 앞의 객체를 검색 문자열로 변환하면 다음과 같습니다.

?page=1&sort=asc&filters=author%3Dtanner%26min_words%3D800

JSURL2 라이브러리 사용

JSURL2는 가독성을 유지하면서 URL을 압축할 수 있는 비표준 라이브러리입니다. 다음 코드로 이를 수행할 수 있습니다.

React

import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
import { parse, stringify } from 'jsurl2'

const router = createRouter({
// ...
parseSearch: parseSearchWith(parse),
stringifySearch: stringifySearchWith(stringify),
})

Solid

import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'
import { parse, stringify } from 'jsurl2'

const router = createRouter({
// ...
parseSearch: parseSearchWith(parse),
stringifySearch: stringifySearchWith(stringify),
})

따라서 이 구성을 사용해 앞의 객체를 검색 문자열로 변환하면 다음과 같습니다.

?page=1&sort=asc&filters=(author~tanner~min*_words~800)~

Zipson 라이브러리 사용

Zipson은 사용하기 쉽고 성능이 뛰어난 JSON 압축 라이브러리입니다(런타임 성능과 최종 압축 성능 모두). 이 라이브러리로 검색 매개변수를 압축하려면 이스케이프/이스케이프 해제와 base64 인코딩/디코딩도 필요하며, 다음 코드를 사용할 수 있습니다.

React

import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
import { stringify, parse } from 'zipson'

const router = createRouter({
parseSearch: parseSearchWith((value) => parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(stringify(value)),
),
})

function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}

function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}

Solid

import {
Router,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/solid-router'
import { stringify, parse } from 'zipson'

const router = createRouter({
parseSearch: parseSearchWith((value) => parse(decodeFromBinary(value))),
stringifySearch: stringifySearchWith((value) =>
encodeToBinary(stringify(value)),
),
})

function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}

function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}

⚠️ 이 스니펫에서 atob/btoa를 사용하지 않는 이유는 무엇인가요?

따라서 이 구성을 사용해 앞의 객체를 검색 문자열로 변환하면 다음과 같습니다.

?page=1&sort=asc&filters=JTdCJUMyJUE4YXV0aG9yJUMyJUE4JUMyJUE4dGFubmVyJUMyJUE4JUMyJUE4bWluX3dvcmRzJUMyJUE4JUMyJUEyQ3UlN0Q%3D

안전한 바이너리 인코딩/디코딩

브라우저에서 atobbtoa 함수가 UTF-8이 아닌 문자와 제대로 작동한다고 보장할 수 없습니다. 대신 다음 인코딩/디코딩 유틸리티를 사용하는 것이 좋습니다.

문자열을 바이너리 문자열로 인코딩하려면:

export function encodeToBinary(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}),
)
}

바이너리 문자열을 문자열로 디코딩하려면:

export function decodeFromBinary(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join(''),
)
}