스왑은 다음과 같습니다
이 README는 진행 중인 작업입니다. 트위터로 질문하실 수도 있습니다.
$ npm i thwack
또는
$ yarn add thwack
Axios는 예전에 출시되었을 때 정말 좋았습니다. 이는 사용하기 어려운 XMLHttpRequest
주위에 약속 기반 래퍼를 제공했습니다. 하지만 그것은 오래전 일이고 시대가 바뀌었습니다. 브라우저가 더욱 똑똑해졌습니다. 이제 데이터 가져오기 솔루션을 따라잡아야 할 때가 온 것일까요?
Thwack은 처음부터 최신 브라우저를 염두에 두고 구축되었습니다. 이 때문에 Axios가 가지고 있는 수하물이 없습니다. Axios의 무게는 약 5,000g 정도입니다. 반면에 Thwack은 ~1.5k로 날씬합니다.
동일한 API를 지원하지만 주로 options
과 관련하여 몇 가지 차이점이 있지만 대부분의 경우 많은 애플리케이션에서 상호 교환적으로 사용할 수 있어야 합니다.
Thwack은 Axios처럼 모든 문제를 해결하려고 하는 것이 아니라 사용자가 실제로 필요한 것의 98%에 대한 솔루션을 제공합니다. 이것이 Thwack의 깃털처럼 가벼운 발자국을 제공하는 것입니다.
긁어보세요. Thwack은 훨씬 더 작은 설치 공간으로 Axios와 동일한 수준의 성능을 제공합니다. 그리고 Thwack의 약속 기반 이벤트 시스템은 사용하기가 더 쉽습니다.
모든 Thwack 인스턴스에서 다음 방법을 사용할 수 있습니다.
thwack(url: string [,options: ThwackOptions]): Promise<ThwackResponse>;
thwack.request(options: ThwackOptions): Promise<ThwackResponse>
thwack.get(url: string [,options: ThwackOptions]): Promise<ThwackResponse>;
thwack.delete(url: string [,options: ThwackOptions]): Promise<ThwackResponse>;
thwack.head(url: string [,options: ThwackOptions]): Promise<ThwackResponse>;
thwack.post(url: string, data:any [,options: ThwackOptions]): Promise<ThwackResponse>;
thwack.put(url: string, data:any [,options: ThwackOptions]): Promise<ThwackResponse>;
thwack.patch(url: string, data:any [,options: ThwackOptions]): Promise<ThwackResponse>;
thwack.create(options: ThwackOptions): ThwackInstance;
create
메소드는 주어진 options
사용하여 현재 Thwack 인스턴스의 새로운 하위 인스턴스를 생성합니다(da!).
thwack.getUri(options: ThwackOptions): string;
Thwaks URL 확인은 RFC-3986을 준수합니다. Axios는 그렇지 않습니다. @thwack/resolve
에 의해 구동됩니다.
Thwack은 request
, response
, data
및 error
이벤트 유형을 지원합니다.
Thwack의 이벤트 시스템에 대한 자세한 내용은 아래의 Thwack 이벤트를 참조하세요.
thwack.addEventListener(type: string, callback: (event:ThwackEvent) => Promise<any> ): void;
thwack.removeEventListener(type: string, callback: (event:ThwackEvent) => Promise<any> ): void;
Thwack에는 동시 요청을 위한 다음과 같은 도우미 기능이 있습니다. 대부분 Axios 호환성을 위한 것입니다. 예제 사용법은 아래 "방법" 섹션을 참조하세요.
thwack.all(Promise<ThwackResponse>[])
thwack.spread(callback<results>)
options
인수에는 다음과 같은 속성이 있습니다.
url
이는 정규화된 URL이거나 상대 URL입니다.
baseURL
위의 url
에서 정규화된 URL을 구축하는 데 사용되는 기본 URL을 정의합니다. 절대 URL이거나 undefined
URL이어야 합니다. 브라우저에서 실행 중이거나 Node 또는 React Native에서 undefined
경우 현재 웹 페이지의 origin
+ pathname
기본값입니다.
예를 들어, 다음과 같이 했다면:
thwack ( 'foo' , {
baseURL : 'http://example.com' ,
} ) ;
가져온 URL은 다음과 같습니다:
http://example.com/foo
method
get
, post
, put
, patch
, delete
또는 head
HTTP 메서드 중 하나를 포함하는 문자열입니다.
data
method
post
, put
또는 patch
인 경우 이는 요청 본문을 작성하는 데 사용되는 데이터입니다.
headers
여기에 선택적 HTTP 요청 헤더를 배치할 수 있습니다. 여기에서 지정하는 모든 헤더는 인스턴스 헤더 값과 병합됩니다.
예를 들어, 다음과 같이 Thwack 인스턴스를 설정하면:
const api = thwack . create ( {
headers : {
'x-app-name' : 'My Awesome App' ,
} ,
} ) ;
그런 다음 나중에 인스턴스를 사용할 때 다음과 같이 호출합니다.
const { data } = await api . get ( 'foo' , {
headers : {
'some-other-header' : 'My Awesome App' ,
} ,
} ) ;
전송될 헤더는 다음과 같습니다.
x-app-name: My Awesome App
some-other-header': 'My Awesome App'
defaults
이를 통해 이 인스턴스 및 사실상 모든 하위 인스턴스에 대한 기본 옵션을 읽고 설정할 수 있습니다.
예:
thwack . defaults . baseURL = 'https://example.com/api' ;
예를 들어 defaults
create
에 전달된 것과 동일한 객체입니다. 예를 들어 다음은 "https://example.com/api"를 출력합니다.
const instance = thwack . create ( {
baseURL : 'https://example.com/api' ,
} ) ;
console . log ( instance . defaults . baseURL ) ;
또한 인스턴스에 defaults
설정하거나 options
을 인스턴스에 전달해도 부모에는 영향을 미치지 않습니다. 따라서 다음 예에서는 thwack.defaults.baseURL
이 여전히 "https://api1.example.net/"입니다.
thwack . defaults . baseURL = 'https://api1.example.net/' ;
const instance = thwack . create ( ) ;
instance . defaults . baseURL = 'https://example.com/api' ;
console . log ( thwack . defaults . baseURL ) ;
params
이는 가져오기 URL을 작성하는 데 사용되는 키/값 쌍을 포함하는 선택적 개체입니다. baseURL
또는 url
에 :key
세그먼트가 있으면 일치하는 키 값으로 대체됩니다. 예를 들어, 다음과 같이 했다면:
thwack ( 'orders/:id' , {
params : { id : 123 } ,
baseURL : 'http://example.com' ,
} ) ;
가져온 URL은 다음과 같습니다:
http://example.com/orders/123
:name
을 지정하지 않거나 :name
보다 param
이 더 많은 경우 나머지 키/값은 검색 매개변수(예: ?key=value
)로 설정됩니다.
maxDepth
Thwack에서 오류가 발생하기 전에 callbck에서 수행할 수 있는 재귀 요청의 최대 수준입니다. 이는 이벤트 콜백으로 인해 재귀 루프가 발생하는 것을 방지하는 데 사용됩니다. 이는 적절한 보호 조치 없이 다른 request
발행하는 경우입니다. 기본값은 3입니다.
responseType
기본적으로 Thwack은 응답 헤더 content-type
값을 기반으로 응답 데이터를 디코딩하는 방법을 자동으로 결정합니다. 그러나 서버가 잘못된 값으로 응답하는 경우 responseType
설정하여 파서를 재정의할 수 있습니다. 유효한 값은 arraybuffer
, document
(예: formdata
), json
, text
, stream
및 blob
입니다. 기본값은 자동입니다.
Thwack이 반환하는 내용은 다음 표에 따라 결정됩니다. "가져오기 방법" 열은 data
에서 해결됩니다. responseType
지정하지 않으면 Thwack은 content-type
및 responseParserMap
테이블(아래 참조)을 기반으로 가져오기 방법을 자동으로 결정합니다.
콘텐츠 유형 | responseType | fetch 방법 |
---|---|---|
application/json | json | response.json() |
multipart/form-data | formdata | response.formData() |
text/event-stream | stream | 처리하지 않고 response.body data 로 다시 전달합니다. |
blob | response.blob() | |
arraybuffer | response.arrayBuffer() | |
*/* | text | response.text() |
참고:
stream
현재 #27741로 인해 React Native에서 지원되지 않습니다.
responseParserMap
사용할 응답 파서를 결정하는 또 다른 유용한 방법은 responseParserMap
사용하는 것입니다. 이를 통해 응답 헤더의 결과 content-type
과 파서 유형 간의 매핑을 설정할 수 있습니다.
Thwack은 json
및 formdata
디코딩을 허용하는 다음 맵을 기본값으로 사용합니다. 일치하는 항목이 없으면 응답 구문 분석기는 기본적으로 text
로 설정됩니다. 특수 */*
키를 설정하여 기본값을 지정할 수 있습니다.
{
"application/json" : " json " ,
"multipart/form-data" : " formdata " ,
"*/*" : " text "
} ;
responseParserMap
에 지정한 모든 값은 기본 맵에 병합됩니다. 즉, 기본값을 무시하거나 새 값을 추가할 수 있습니다.
예를 들어 이미지를 Blob으로 다운로드한다고 가정해 보겠습니다. baseURL
API 엔드포인트로 설정하고 모든 유형의 이미지를 Blob으로 다운로드하는 responseParserMap
을 설정할 수 있지만 여전히 json
다운로드를 허용합니다( content-type: application/json
의 기본값이므로).
import thwack from 'thwack' ;
thwack . defaults . responseParserMap = { 'image/*' : 'blob' } ;
image/*
콘텐츠 유형(예: image/jpeg
, image/png
등)으로 다운로드한 모든 URL은 blob
파서로 구문 분석됩니다.
const getBlobUrl = async ( url ) => {
const blob = ( await thwack . get ( url ) ) . data ;
const objectURL = URL . createObjectURL ( blob ) ;
return objectURL ;
} ;
CodeSandbox에서 실행되는 이 예제를 참조하세요.
이미지 이외의 다른 것에 이 기술을 사용할 수 있습니다.
보시다시피, responseParserMap
사용하는 것은 다양한 Thwack 호출에 대해 responseType
설정할 필요를 없애는 좋은 방법입니다.
validateStatus
이 선택적 함수는 Thwack이 약속을 반환하거나 던지기 위해 사용하는 상태 코드를 결정하는 데 사용됩니다. 응답 status
전달됩니다. 이 함수가 진실을 반환하면 약속이 해결되고, 그렇지 않으면 약속이 거부됩니다.
기본 기능은 2xx(예: 200-299)에 없는 모든 상태에 대해 발생합니다.
paramsSerializer
이것은 Thwack이 params
직렬화하기 위해 호출하는 선택적 함수입니다. 예를 들어, 객체 {a:1, b:2, foo: 'bar'}
가 있으면 문자열 a=1&b=2&foo=bar
로 직렬화해야 합니다.
대부분의 사람들에게는 기본 직렬 변환기가 제대로 작동합니다. 이는 주로 엣지 케이스와 Axios 호환성을 위한 것입니다.
기본 직렬 변환기는 매개변수를 알파벳순으로 정렬하므로 따라하는 것이 좋습니다. 그러나 이것이 귀하의 상황에 맞지 않으면 자체 직렬 변환기를 롤링할 수 있습니다.
resolver
이는 기본 확인자 동작을 재정의하기 위해 제공할 수 있는 함수입니다. resolver
정의되지 않은 url
과 baseURL
또는 절대 URL이라는 두 가지 인수를 사용합니다. 리졸버를 교체할 이유가 거의 없지만 필요할 경우를 대비해 탈출구가 됩니다.
status
수신된 3자리 HTTP 상태 코드를 나타내는 number
.
ok
true로 설정된 boolean
2xx 범위(즉, 성공)의 status
코드입니다. 이 값은 validateStatus
의 영향을 받지 않습니다.
statusText
status
코드의 텍스트를 나타내는 string
입니다. 모든 프로그램 로직에서는 status
코드(또는 ok
)를 사용해야 합니다.
headers
반환된 HTTP 헤더가 있는 키/값 개체입니다. 중복된 헤더는 세미콜론으로 구분된 단일 헤더로 연결됩니다.
data
이는 스트리밍 및 변환된 후 HTTP 응답의 반환된 본문을 보유합니다. 유일한 예외는 stream
의 responseType
사용한 경우입니다. 이 경우 data
body
요소에 직접 설정됩니다.
ThwackResponseError
발생한 경우 data
응답 본문의 일반 텍스트 표현이 됩니다.
options
요청을 처리한 전체 options
개체입니다. 이 options
모든 상위 인스턴스 및 defaults
과 완전히 병합됩니다.
response
fetch
에 의해 반환된 전체 HTTP Response
개체 또는 합성 이벤트 콜백의 response
입니다.
Thwack 요청의 응답으로 인해 2xx가 아닌 status
코드(예: 404 Not Found)가 발생하면 ThwackResponseError
가 발생합니다.
참고: 다른 유형의 오류(예: 잘못된 이벤트 리스너 콜백)가 발생할 수 있으므로 발견된 오류를 조사하여
ThwackResponseError
유형인지 확인하는 것이 가장 좋습니다.
try {
const { data } = await thwack . get ( someUrl )
} catch ( ex ) {
if ( ex instanceof thwack . ThwackResponseError )
const { status , message } = ex ;
console . log ( `Thwack status ${ status } : ${ message } ` ) ;
} else {
throw ex ; // If not, rethrow the error
}
}
ThwackResponseError
에는 일반 JavaScript Error
의 모든 속성과 성공 상태와 동일한 속성을 가진 thwackResponse
속성이 있습니다.
Thwack에서 생성된 인스턴스는 상위 인스턴스를 기반으로 합니다. 부모의 기본 옵션은 인스턴스를 통해 전달됩니다. 이는 baseURL
과 같이 하위 항목에 영향을 미칠 수 있는 상위 옵션을 설정하는 데 유용할 수 있습니다.
반대로, 부모는 addEventListener
사용하여 자녀를 모니터링할 수 있습니다(이에 대한 예는 아래의 모든 API 호출을 기록하는 방법 참조).
인스턴스와 결합된 Thwack 이벤트 시스템은 Thwack을 매우 강력하게 만듭니다. 이를 통해 다양한 이벤트를 들을 수 있습니다.
모든 이벤트에 대한 이벤트 흐름은 다음과 같습니다. 보시다시피, 콜백이 이미 수행되었는지 확인하지 않고 맹목적으로 request()
발행하는 경우 코드가 무한 루프에 빠질 수 있으므로 주의하십시오.
request
이벤트 애플리케이션의 일부가 데이터 가져오기 메서드 중 하나를 호출할 때마다 request
이벤트가 발생합니다. 모든 리스너는 event.options
에 호출 options
이 있는 ThwackRequestEvent
객체를 가져옵니다. 이러한 이벤트 리스너는 간단한 작업(이벤트 기록) 또는 요청을 방지하고 응답을 반환하는 작업(모의 데이터)과 같은 복잡한 작업을 수행할 수 있습니다.
// callback will be called for every request made in Thwack
thwack . addEventListener ( 'request' , callback ) ;
콜백은
async
일 수 있으므로 Thwack을 연기하여 진행하기 전에 나가서 다른 URL로 데이터를 가져올 수 있습니다.
response
이벤트 이벤트는 HTTP 헤더가 수신된 후 , 본문이 스트리밍 및 구문 분석 되기 전에 시작됩니다. 리스너는 thwackResponse
키가 응답으로 설정된 ThwackResponseEvent
객체를 수신합니다.
data
이벤트 본문이 스트리밍되고 구문 분석된 후에 이벤트가 시작됩니다. 가져오기가 2xx 상태 코드를 반환한 경우에만 실행됩니다. 리스너는 thwackResponse
키가 응답으로 설정된 ThwackDataEvent
객체를 수신합니다.
error
이벤트 본문이 스트리밍되고 구문 분석된 후에 이벤트가 시작됩니다. 가져오기에서 2xx가 아닌 상태 코드를 반환하면 시작됩니다. 리스너는 thwackResponse
키가 응답으로 설정된 ThwackErrorEvent
객체를 수신합니다.
Thwack은 NodeJS에서 작동하지만 window.fetch
에 대한 폴리필이 필요합니다. 다행히도 사용할 수 있는 node-fetch
라는 멋진 폴리필이 있습니다.
NodeJS 버전 10을 사용하는 경우 Array#flat
및 Object#fromEntries
에 대한 폴리필도 필요합니다. NodeJS 버전 11+에는 이러한 메서드가 있으며 폴리필이 필요하지 않습니다.
이러한 폴리필을 직접 제공하거나 대신 다음 편의 가져오기 중 하나를 사용할 수 있습니다. NodeJS 11+를 실행하는 경우 다음을 사용하세요.
import thwack from 'thwack/node' ; // NodeJS version 12+
NodeJS 10에서 실행 중인 경우 다음을 사용하세요.
import thwack from 'thwack/node10' ; // NodeJS version 10
이러한 폴리필을 직접 제공하려면 Thwack을 사용하려면 thwack/core
에서 가져와서 fetch
fetch
기본값으로 설정해야 합니다.
import thwack from 'thwack/code' ;
thwack . defaults . fetch = global . fetch ;
이 작업은 앱 시작 코드(일반적으로 index.js
에서 수행되어야 합니다.
참고:
blob
의responseType
NodeJS에서 지원되지 않습니다.
Thwack은 React Native와 호환되며 추가 폴리필이 필요하지 않습니다. React Native로 작성된 샘플 앱은 아래를 참조하세요.
참고: React Native는 #27741로 인해
stream
지원하지 않습니다.
thwack.all()
및 thwack.spread()
사용하여 동시 요청을 할 수 있습니다. 그러면 데이터가 하나의 배열로 콜백에 표시됩니다.
여기에는 두 명의 GitHub 사용자에 대한 정보가 표시됩니다.
function displayGitHubUsers ( ) {
return thwack
. all ( [
thwack . get ( 'https://api.github.com/users/donavon' ) ,
thwack . get ( 'https://api.github.com/users/revelcw' ) ,
] )
. then (
thwack . spread ( ( ... results ) => {
const output = results
. map (
( { data } ) => ` ${ data . login } has ${ data . public_repos } public repos`
)
. join ( 'n' ) ;
console . log ( output ) ;
} )
) ;
}
이는 단순히 도우미 함수라는 점에 유의하세요. async
/ await
사용하는 경우 Promise.all
사용하여 Thwack 도우미 없이 이를 작성할 수 있습니다.
async function displayGitHubUsers ( ) {
const results = await Promise . all ( [
thwack . get ( 'https://api.github.com/users/donavon' ) ,
thwack . get ( 'https://api.github.com/users/revelcw' ) ,
] ) ;
const output = results
. map ( ( { data } ) => ` ${ data . login } has ${ data . public_repos } public repos` )
. join ( 'n' ) ;
console . log ( output ) ;
}
CodeSandbox에서 실시간으로 실행되는 모습을 볼 수 있습니다.
(axios/fetch의 이 blob 게시물에서 영감을 받은 데모)
AbortController
사용하면 thwack
옵션에 signal
를 전달하여 요청을 취소할 수 있습니다.
브라우저에서 내장된 AbortController를 사용할 수 있습니다.
import thwack from 'thwack' ;
const controller = new AbortController ( ) ;
const { signal } = controller ;
thwack ( url , { signal } ) . then ( handleResponse ) . catch ( handleError ) ;
controller . abort ( ) ;
NodeJS에서는 abort-controller와 같은 것을 사용할 수 있습니다.
import thwack from 'thwack' ;
import AbortController from 'abort-controller' ;
const controller = new AbortController ( ) ;
const { signal } = controller ;
thwack ( url , { signal } ) . then ( handleResponse ) . catch ( handleError ) ;
controller . abort ( ) ;
요청 취소 시 어떤 작업을 수행하려는 경우 signal
에 대한 abort
이벤트도 들을 수 있습니다.
signal . addEventListener ( 'abort' , handleAbort ) ;
addEventListener('request', callback)
추가하고 각 요청을 콘솔에 기록합니다.
import thwack from 'thwack' ;
thwack . addEventListener ( 'request' , ( event ) => {
console . log ( 'hitting URL' , thwack . getUri ( event . options ) ) ;
} ) ;
React를 사용하는 경우 동일한 작업을 수행하는 앱에서 "사용"할 수 있는 Hook이 있습니다.
import { useEffect } from 'react' ;
import thwack from 'thwack' ;
const logUrl = ( event ) => {
const { options } = event ;
const fullyQualifiedUrl = thwack . getUri ( options ) ;
console . log ( `hitting ${ fullyQualifiedUrl } ` ) ;
} ;
const useThwackLogger = ( ) => {
useEffect ( ( ) => {
thwack . addEventListener ( 'request' , logUrl ) ;
return ( ) => thwack . removeEventListener ( 'request' , logUrl ) ;
} , [ ] ) ;
} ;
export default useThwackLogger ;
사용 방법에 대한 코드 조각은 다음과 같습니다.
const App = ( ) = {
useThwackLogger ( )
return (
< div >
...
</ div >
)
}
일부 사용자 데이터를 요청한 앱이 있다고 가정해 보겠습니다. 앱이 특정 URL(예: users
)에 도달하고 특정 사용자 ID(예: 123
)를 쿼리하는 경우 요청이 서버에 도달하는 것을 방지하고 대신 결과를 모의하는 것이 좋습니다.
ThwackResponse
의 status
는 기본적으로 200이므로 OK가 아닌 응답을 모의 처리할 필요가 없으면 data
만 반환하면 됩니다.
thwack . addEventListener ( 'request' , async ( event ) => {
const { options } = event ;
if ( options . url === 'users' && options . params . id === 123 ) {
// tells Thwack to use the returned value instead of handling the event itself
event . preventDefault ( ) ;
// stop other listeners (if any) from further processing
event . stopPropagation ( ) ;
// because we called `preventDefault` above, the caller's request
// will be resolved to this `ThwackResponse` (defaults to status of 200 and ok)
return new thwack . ThwackResponse (
{
data : {
name : 'Fake Username' ,
email : '[email protected]' ,
} ,
} ,
options
) ;
}
} ) ;
DTO(Data Transfer Object)를 클라이언트가 사용하기 쉬운 것으로 변환하는 것이 바람직한 경우가 많습니다. 아래 예에서는 복잡한 DTO를 firstName
, lastName
, avatar
및 email
로 변환합니다. API 호출에서 반환되었지만 애플리케이션에 필요하지 않은 기타 데이터 요소는 무시됩니다.
이 샘플 앱에서 DTO 변환, 로깅 및 가짜 데이터 반환의 예를 볼 수 있습니다.
CodeSandbox에서 소스코드를 보실 수 있습니다.
이 예에는 이미지를 Blob URL로 로드하는 React Hook이 있습니다. 세션 저장소에 Blob URL 매핑에 대한 URL을 캐시합니다. 로드된 후 페이지를 새로 고치면 Blob URL에서 이미지가 즉시 로드됩니다.
const useBlobUrl = ( imageUrl ) => {
const [ objectURL , setObjectURL ] = useState ( '' ) ;
useEffect ( ( ) => {
let url = sessionStorage . getItem ( imageUrl ) ;
async function fetchData ( ) {
if ( ! url ) {
const { data } = await thwack . get ( imageUrl , {
responseType : 'blob' ,
} ) ;
url = URL . createObjectURL ( data ) ;
sessionStorage . setItem ( imageUrl , url ) ;
}
setObjectURL ( url ) ;
}
fetchData ( ) ;
} , [ imageUrl ] ) ;
return objectURL ;
} ;
CodeSandbox에서 이 예를 참조하세요.
현재 https://api.example.com
에 REST 엔드포인트가 있습니다. 새로운 REST 엔드포인트를 다른 URL에 게시했으며 네트워크 트래픽의 2%를 이러한 새 서버로 천천히 라우팅하기 시작한다고 가정해 보겠습니다.
참고: 일반적으로 이는 백엔드의 로드 밸런서에 의해 처리됩니다. 여기에는 데모용으로만 표시되어 있습니다.
요청 이벤트 리스너의 options.url
다음과 같이 교체하면 이를 수행할 수 있습니다.
thwack . addEventListener ( 'request' , ( event ) => {
if ( Math . random ( ) >= 0.02 ) {
return ;
}
// the code will be executed for approximately 2% of the requests
const { options } = event ;
const oldUrl = thwack . getUri ( options ) ;
const url = new URL ( '' , oldUrl ) ;
url . origin = 'https://api2.example.com' ; // point the origin at the new servers
const newUrl = url . href ; // Get the fully qualified URL
event . options = { ... event . options , url : newUrl } ; // replace `options`]
} ) ;
use-thwack
과 함께 React Native용 데이터 가져오기 앱을 작성하는 것이 이보다 쉬울 수는 없습니다.
Expo에서 실행되는 전체 앱을 확인하세요.
Thwack은 Axios에서 많은 영감을 받았습니다. 고마워요 매트!
MIT 라이선스
멋진 분들께 감사드립니다(이모티콘 키):
도나본 웨스트 ? | 제레미 타이스 | 유라이마 에스테베즈 | 제레미 바가 | 브룩 스칼렛 얄로프 | 칼 호키 | 코지 |
톰 바이러 | 이안 서덜랜드 | 블레이크 요더 | 라이언 힌치 | 미로 도이키치 | 산티체비치 |
이 프로젝트는 모든 기여자 사양을 따릅니다. 어떤 종류의 기여도 환영합니다!