PHP 7.2 이상의 큰 JSON 파일 또는 스트림의 비효율적인 반복을 위한 사용하기 매우 쉽고 메모리 효율적인 드롭인 대체입니다. 요약을 참조하세요. 선택적 ext-json
제외하고 프로덕션에는 종속성이 없습니다. 코드와 동기화된 README
버전 1.2.0
의 새로운 기능 - 재귀적 반복
<?php
use JsonMachineItems;
// this often causes Allowed Memory Size Exhausted,
// because it loads all the items in the JSON into memory
- $users = json_decode(file_get_contents('500MB-users.json'));
// this has very small memory footprint no matter the file size
// because it loads items into memory one by one
+ $users = Items::fromFile('500MB-users.json');
foreach ($users as $id => $user) {
// just process $user as usual
var_dump($user->name);
}
$users[42]
와 같은 무작위 액세스는 아직 불가능합니다. 위에서 언급한 foreach
사용하여 항목을 찾거나 JSON 포인터를 사용하세요.
iterator_count($users)
를 통해 항목 수를 계산합니다. 개수를 얻으려면 내부적으로 전체를 반복해야 하므로 반복하고 손으로 계산하는 것과 거의 같은 시간이 소요됩니다.
기본적으로 사용되는 경우 ext-json
필요하지만 사용자 정의 디코더가 사용되는 경우에는 필요하지 않습니다. 디코더를 참조하세요.
변경로그를 팔로우하세요.
JSON Machine은 예측할 수 없을 정도로 긴 JSON 스트림 또는 문서용으로 개발된 생성기를 기반으로 하는 효율적이고 사용하기 쉽고 빠른 JSON 스트림/풀/증분/게으른(무슨 이름을 지정하든) 파서입니다. 주요 기능은 다음과 같습니다:
foreach
사용하여 모든 크기의 JSON을 반복하면 됩니다. 이벤트와 콜백이 없습니다.json_decode
사용하여 JSON 문서 항목을 디코딩합니다. 디코더를 참조하세요. fruits.json
에 다음과 같은 거대한 JSON 문서가 포함되어 있다고 가정해 보겠습니다.
// fruits.json
{
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
다음과 같이 구문 분석할 수 있습니다.
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' );
foreach ( $ fruits as $ name => $ data ) {
// 1st iteration: $name === "apple" and $data->color === "red"
// 2nd iteration: $name === "pear" and $data->color === "yellow"
}
json 객체 대신 json 배열을 구문 분석하는 것도 동일한 논리를 따릅니다. foreach의 키는 항목의 숫자 인덱스입니다.
JSON Machine이 객체 대신 배열을 반환하도록 하려면 new ExtJsonDecoder(true)
디코더로 사용하세요.
<?php
use JsonMachine JsonDecoder ExtJsonDecoder ;
use JsonMachine Items ;
$ objects = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ExtJsonDecoder ( true )]);
이 fruits.json
에서 results
하위 트리만 반복하려면 다음을 수행하세요.
// fruits.json
{
"results" : {
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
}
JSON 포인터 /results
pointer
옵션으로 사용합니다.
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /results ' ]);
foreach ( $ fruits as $ name => $ data ) {
// The same as above, which means:
// 1st iteration: $name === "apple" and $data->color === "red"
// 2nd iteration: $name === "pear" and $data->color === "yellow"
}
메모:
results
값은 한 번에 메모리에 로드되지 않고results
에서는 한 번에 하나의 항목만 로드됩니다. 현재 반복 중인 수준/하위 트리에서 한 번에 메모리에 있는 항목은 항상 하나입니다. 따라서 메모리 소비는 일정합니다.
JSON 포인터 사양에서는 특정 배열 인덱스 대신 하이픈( -
)을 사용할 수도 있습니다. JSON Machine은 이를 모든 배열 인덱스 (객체 키가 아님)와 일치하는 와일드카드로 해석합니다. 이를 통해 전체 항목을 로드하지 않고도 배열의 중첩된 값을 반복할 수 있습니다.
예:
// fruitsArray.json
{
"results" : [
{
"name" : " apple " ,
"color" : " red "
},
{
"name" : " pear " ,
"color" : " yellow "
}
]
}
과일의 모든 색상을 반복하려면 JSON 포인터 "/results/-/color"
를 사용하세요.
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruitsArray.json ' , [ ' pointer ' => ' /results/-/color ' ]);
foreach ( $ fruits as $ key => $ value ) {
// 1st iteration:
$ key == ' color ' ;
$ value == ' red ' ;
$ fruits -> getMatchedJsonPointer () == ' /results/-/color ' ;
$ fruits -> getCurrentJsonPointer () == ' /results/0/color ' ;
// 2nd iteration:
$ key == ' color ' ;
$ value == ' yellow ' ;
$ fruits -> getMatchedJsonPointer () == ' /results/-/color ' ;
$ fruits -> getCurrentJsonPointer () == ' /results/1/color ' ;
}
컬렉션과 동일한 방식으로 문서의 어느 위치에서나 단일 스칼라 값을 구문 분석할 수 있습니다. 다음 예를 고려하십시오.
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
},
// ... gigabytes follow ...
}
다음과 같이 lastModified
키의 스칼라 값을 가져옵니다.
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
foreach ( $ fruits as $ key => $ value ) {
// 1st and final iteration:
// $key === 'lastModified'
// $value === '2012-12-12'
}
파서가 값을 찾아서 사용자에게 제공하면 파싱이 중지됩니다. 따라서 단일 스칼라 값이 기가바이트 크기의 파일이나 스트림의 시작 부분에 있으면 메모리를 거의 사용하지 않고 즉시 처음부터 값을 가져옵니다.
확실한 지름길은 다음과 같습니다:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
$ lastModified = iterator_to_array ( $ fruits )[ ' lastModified ' ];
단일 스칼라 값 액세스는 JSON 포인터의 배열 인덱스도 지원합니다.
여러 JSON 포인터를 사용하여 여러 하위 트리를 구문 분석하는 것도 가능합니다. 다음 예를 고려하십시오.
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"berries" : [
{
"name" : " strawberry " , // not a berry, but whatever ...
"color" : " red "
},
{
"name" : " raspberry " , // the same ...
"color" : " red "
}
],
"citruses" : [
{
"name" : " orange " ,
"color" : " orange "
},
{
"name" : " lime " ,
"color" : " green "
}
]
}
모든 베리와 감귤류를 반복하려면 JSON 포인터 ["/berries", "/citrus"]
사용하세요. 포인터의 순서는 중요하지 않습니다. 항목은 문서에 나타나는 순서대로 반복됩니다.
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [
' pointer ' => [ ' /berries ' , ' /citruses ' ]
]);
foreach ( $ fruits as $ key => $ value ) {
// 1st iteration:
$ value == [ " name " => " strawberry " , " color " => " red " ];
$ fruits -> getCurrentJsonPointer () == ' /berries ' ;
// 2nd iteration:
$ value == [ " name " => " raspberry " , " color " => " red " ];
$ fruits -> getCurrentJsonPointer () == ' /berries ' ;
// 3rd iteration:
$ value == [ " name " => " orange " , " color " => " orange " ];
$ fruits -> getCurrentJsonPointer () == ' /citruses ' ;
// 4th iteration:
$ value == [ " name " => " lime " , " color " => " green " ];
$ fruits -> getCurrentJsonPointer () == ' /citruses ' ;
}
JSON 구조가 Items
및 JSON 포인터로 처리하기 어렵거나 불가능하거나 반복하는 개별 항목이 너무 커서 처리할 수 없는 경우 Items
대신 RecursiveItems
사용하세요. 반면에 Items
보다 속도가 현저히 느리므로 이를 염두에 두십시오.
RecursiveItems
가 JSON에서 목록이나 사전을 발견하면 반복할 수 있는 자체의 새 인스턴스를 반환하고 주기가 반복됩니다. 따라서 PHP 배열이나 객체를 반환하지 않고 스칼라 값이나 RecursiveItems
만 반환합니다. JSON 사전이나 목록은 한 번에 메모리에 완전히 로드되지 않습니다.
아주 많은 친구가 있는 아주 많은 사용자의 예를 살펴보겠습니다.
// users.json
[
{
"username" : " user " ,
"e-mail" : " [email protected] " ,
"friends" : [
{
"username" : " friend1 " ,
"e-mail" : " [email protected] "
},
{
"username" : " friend2 " ,
"e-mail" : " [email protected] "
}
]
}
]
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user as $ field => $ value ) {
if ( $ field === ' friends ' ) {
/** @var $value RecursiveItems */
foreach ( $ value as $ friend ) {
/** @var $friend RecursiveItems */
foreach ( $ friend as $ friendField => $ friendValue ) {
$ friendField == ' username ' ;
$ friendValue == ' friend1 ' ;
}
}
}
}
}
게으른 더 깊은 수준의 반복을 중단하고(예:
break
통해 일부"friends"
건너뛰기) 다음 값(예: nextuser
)으로 진행하면 나중에 반복할 수 없습니다. JSON Machine은 다음 값을 읽을 수 있도록 백그라운드에서 이를 반복해야 합니다. 이러한 시도로 인해 생성기 폐쇄 예외가 발생합니다.
RecursiveItems
의 편의 메서드 toArray(): array
RecursiveItems의 특정 인스턴스가 메모리 관리 가능한 데이터 구조(예: $friend)를 가리키는 것이 확실하다면 $friend->toArray()
호출할 수 있으며 항목은 일반 PHP 배열.
advanceToKey(int|string $key): scalar|RecursiveItems
컬렉션에서 특정 키(예: $user
의 'friends'
)를 검색할 때 루프와 조건을 사용하여 검색할 필요가 없습니다. 대신 $user->advanceToKey("friends")
호출하면 됩니다. 반복하여 이 키의 값을 반환합니다. 통화는 연결될 수 있습니다. 또한 다음 인덱스로 이동하고 가져오기 위한 구문과 같은 배열을 지원합니다. 따라서 $user['friends']
$user->advanceToKey('friends')
의 별칭이 됩니다. 통화는 연결될 수 있습니다. 이는 단지 별칭일 뿐이라는 점을 명심하세요. 이를 RecursiveItems
에서 직접 사용한 후에 는 이전 인덱스에 무작위로 액세스할 수 없습니다 . 그것은 단지 구문 설탕 일뿐입니다. 레코드/항목의 색인에 대한 무작위 액세스가 필요한 경우 toArray()
사용하십시오.
따라서 이전 예는 다음과 같이 단순화될 수 있습니다.
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user [ ' friends ' ] as $ friend ) { // or $user->advanceToKey('friends')
/** @var $friend RecursiveItems */
$ friendArray = $ friend -> toArray ();
$ friendArray [ ' username ' ] === ' friend1 ' ;
}
}
체인을 사용하면 다음과 같은 작업을 수행할 수 있습니다.
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
$ users [ 0 ][ ' friends ' ][ 1 ][ ' username ' ] === ' friend2 ' ;
RecursiveItems implements RecursiveIterator
따라서 예를 들어 PHP의 내장 도구를 사용하여 다음과 같이 RecursiveIterator
작업할 수 있습니다.
JSON 문서에서 하나의 항목을 처리하는 방법입니다. JSON 포인터 RFC 6901을 참조하세요. 때로는 JSON 구조가 더 깊어지고 기본 수준이 아닌 하위 트리를 반복하기를 원하기 때문에 매우 편리합니다. 따라서 반복하려는 JSON 배열이나 개체(또는 스칼라 값)에 대한 포인터를 지정하면 됩니다. 파서가 지정한 컬렉션에 도달하면 반복이 시작됩니다. 모든 Items::from*
함수에서 pointer
옵션으로 전달할 수 있습니다. 문서에 존재하지 않는 위치에 대한 포인터를 지정하면 예외가 발생합니다. 스칼라 값에 액세스하는 데에도 사용할 수 있습니다. JSON 포인터 자체는 유효한 JSON 문자열이어야 합니다 . 참조 토큰(슬래시 사이 부분)의 리터럴 비교는 JSON 문서 키/멤버 이름에 대해 수행됩니다.
몇 가지 예:
JSON 포인터 값 | 반복됩니다 |
---|---|
(빈 문자열 - 기본값) | ["this", "array"] 또는 {"a": "this", "b": "object"} 가 반복됩니다(기본 수준). |
/result/items | {"result": {"items": ["this", "array", "will", "be", "iterated"]}} |
/0/items | [{"items": ["this", "array", "will", "be", "iterated"]}] (배열 인덱스 지원) |
/results/-/status | {"results": [{"status": "iterated"}, {"status": "also iterated"}]} (배열 인덱스 와일드카드로서의 하이픈) |
/ (알았어요! - 슬래시 뒤에 빈 문자열이 옵니다. 사양을 참조하세요.) | {"":["this","array","will","be","iterated"]} |
/quotes" | {"quotes"": ["this", "array", "will", "be", "iterated"]} |
옵션은 JSON이 구문 분석되는 방식을 변경할 수 있습니다. 옵션 배열은 모든 Items::from*
함수의 두 번째 매개변수입니다. 사용 가능한 옵션은 다음과 같습니다.
pointer
- 문서의 어느 부분을 반복할지 알려주는 JSON 포인터 문자열입니다.decoder
- ItemDecoder
인터페이스의 인스턴스입니다.debug
- 디버그 모드를 활성화하거나 비활성화하려면 true
또는 false
. 디버그 모드가 활성화되면 구문 분석 중이나 예외 중에 문서의 줄, 열, 위치와 같은 데이터를 사용할 수 있습니다. 디버그를 비활성화하면 성능이 약간 향상됩니다. 스트림 API 응답 또는 기타 JSON 스트림은 파일과 동일한 방식으로 구문 분석됩니다. 유일한 차이점은 Items::fromStream($streamResource)
사용한다는 것입니다. 여기서 $streamResource
는 JSON 문서가 있는 스트림 리소스입니다. 나머지는 파일 파싱과 동일합니다. 스트리밍 응답을 지원하는 인기 있는 http 클라이언트의 몇 가지 예는 다음과 같습니다.
Guzzle은 자체 스트림을 사용하지만 GuzzleHttpPsr7StreamWrapper::getResource()
호출하여 다시 PHP 스트림으로 변환할 수 있습니다. 이 함수의 결과를 Items::fromStream
함수에 전달하면 설정이 완료됩니다. GuzzleHttp 예제 작업을 참조하세요.
Symfony HttpClient의 스트림 응답은 반복자로 작동합니다. JSON Machine은 반복자를 기반으로 하기 때문에 Symfony HttpClient와의 통합이 매우 간단합니다. HttpClient 예제를 참조하세요.
debug
활성화) 큰 문서는 구문 분석하는 데 시간이 걸릴 수 있습니다. foreach
에서 Items::getPosition()
호출하여 처음부터 처리된 바이트의 현재 수를 가져옵니다. 그러면 백분율은 position / total * 100
으로 쉽게 계산됩니다. 문서의 전체 크기를 바이트 단위로 확인하려면 다음을 확인하세요.
strlen($document)
문자열을 구문 분석하는 경우filesize($file)
Content-Length
http 헤더 debug
비활성화된 경우 getPosition()
항상 0
반환합니다.
<?php
use JsonMachine Items ;
$ fileSize = filesize ( ' fruits.json ' );
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' debug ' => true ]);
foreach ( $ fruits as $ name => $ data ) {
echo ' Progress: ' . intval ( $ fruits -> getPosition () / $ fileSize * 100 ) . ' % ' ;
}
Items::from*
함수도 decoder
옵션을 허용합니다. JsonMachineJsonDecoderItemDecoder
의 인스턴스여야 합니다. 아무것도 지정하지 않으면 기본적으로 ExtJsonDecoder
사용됩니다. json_decode
사용하기 때문에 ext-json
PHP 확장이 필요합니다. json_decode
원하는 대로 작동하지 않으면 JsonMachineJsonDecoderItemDecoder
구현하고 직접 만드세요.
ExtJsonDecoder
- 기본값. json_decode
사용하여 키와 값을 디코딩합니다. 생성자에는 json_decode
와 동일한 매개변수가 있습니다.
PassThruDecoder
- 디코딩을 수행하지 않습니다. 키와 값은 모두 순수 JSON 문자열로 생성됩니다. foreach에서 직접 다른 항목으로 JSON 항목을 구문 분석하고 JsonMachineJsonDecoderItemDecoder
구현하고 싶지 않을 때 유용합니다. 1.0.0
부터 json_decode
사용하지 않습니다.
예:
<?php
use JsonMachine JsonDecoder PassThruDecoder ;
use JsonMachine Items ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new PassThruDecoder ]);
ErrorWrappingDecoder
- DecodingError
개체 내에서 디코딩 오류를 래핑하여 SyntaxError
예외로 인해 죽는 대신 잘못된 형식의 항목을 건너뛸 수 있도록 하는 데코레이터입니다. 예: <?php
use JsonMachine Items ;
use JsonMachine JsonDecoder DecodingError ;
use JsonMachine JsonDecoder ErrorWrappingDecoder ;
use JsonMachine JsonDecoder ExtJsonDecoder ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ErrorWrappingDecoder ( new ExtJsonDecoder ())]);
foreach ( $ items as $ key => $ item ) {
if ( $ key instanceof DecodingError || $ item instanceof DecodingError) {
// handle error of this malformed json item
continue ;
}
var_dump ( $ key , $ item );
}
0.4.0부터 모든 예외는 JsonMachineException
확장하므로 이를 포착하여 JSON Machine 라이브러리에서 오류를 필터링할 수 있습니다.
json 스트림에 오류가 있으면 SyntaxError
예외가 발생합니다. 하나의 json 항목 내에 오류가 있는 경우 하나의 잘못된 항목으로 인해 문서의 나머지 부분을 구문 분석할 수 없기 때문에 이는 매우 불편합니다. ErrorWrappingDecoder
는 이를 도와줄 수 있는 디코더 데코레이터입니다. 디코더를 그것으로 감싸면 반복하는 모든 잘못된 항목이 DecodingError
통해 foreach에서 제공됩니다. 이렇게 하면 해당 항목을 건너뛰고 문서를 계속 진행할 수 있습니다. 사용 가능한 디코더의 예를 참조하세요. 반복된 항목 사이의 json 스트림 구조에서 구문 오류가 발생하면 여전히 SyntaxError
예외가 발생합니다.
시간 복잡도는 항상 O(n)
입니다.
요약: 메모리 복잡도는 O(2)
입니다.
JSON Machine은 스트림(또는 파일)을 한 번에 1개의 JSON 항목을 읽고 한 번에 해당하는 1개의 PHP 항목을 생성합니다. 이것이 가장 효율적인 방법입니다. JSON 파일에 10,000명의 사용자가 있고 json_decode(file_get_contents('big.json'))
사용하여 이를 구문 분석하려는 경우 메모리에 전체 문자열과 10,000명의 사용자가 모두 있기 때문입니다. PHP 구조. 다음 표는 차이점을 보여줍니다.
한 번에 메모리의 문자열 항목 | 한 번에 메모리에 있는 PHP 항목을 디코딩함 | 총 | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::from*() | 1 | 1 | 2 |
즉, JSON Machine은 처리된 JSON의 크기에 관계없이 지속적으로 효율적입니다. 100GB는 문제 없습니다.
요약: 메모리 복잡도는 O(n+1)
입니다.
Items::fromString()
메서드도 있습니다. 큰 문자열을 강제로 구문 분석해야 하는데 스트림을 사용할 수 없는 경우 JSON Machine이 json_decode
보다 나을 수 있습니다. 그 이유는 json_decode
와 달리 JSON Machine은 여전히 JSON 문자열을 한 번에 한 항목씩 탐색하고 모든 결과 PHP 구조를 한 번에 메모리에 로드하지 않기 때문입니다.
사용자가 10,000명인 경우의 예를 계속 진행하겠습니다. 이번에는 모두 메모리의 문자열에 있습니다. 해당 문자열을 json_decode
로 디코딩하면 메모리에 10,000개의 배열(객체)이 생성되고 결과가 반환됩니다. 반면에 JSON Machine은 문자열에서 발견된 각 항목에 대해 단일 구조를 생성하여 사용자에게 반환합니다. 이 항목을 처리하고 다음 항목으로 반복하면 또 다른 단일 구조가 생성됩니다. 이는 스트림/파일과 동일한 동작입니다. 다음 표에서는 개념을 관점으로 보여줍니다.
한 번에 메모리의 문자열 항목 | 한 번에 메모리에 있는 PHP 항목 디코딩 | 총 | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::fromString() | 10000 | 1 | 10001 |
현실은 더욱 좋습니다. Items::fromString
json_decode
보다 약 5배 적은 메모리를 소비합니다. 그 이유는 PHP 구조가 해당 JSON 표현보다 훨씬 더 많은 메모리를 사용하기 때문입니다.
이유 중 하나는 반복하려는 항목이 "results"
와 같은 일부 하위 키에 있지만 JSON 포인터를 지정하는 것을 잊었기 때문일 수 있습니다. 하위 트리 구문 분석을 참조하세요.
또 다른 이유는 반복하는 항목 중 하나가 너무 커서 한 번에 디코딩할 수 없기 때문일 수 있습니다. 예를 들어, 사용자를 반복하고 그 중 한 명에게 수천 개의 "친구" 개체가 있다고 가정해 보겠습니다. 가장 효율적인 솔루션은 재귀 반복을 사용하는 것입니다.
이는 아마도 단일 JSON 스칼라 문자열 자체가 너무 커서 메모리에 맞지 않는다는 의미일 수 있습니다. 예를 들어 매우 큰 base64 인코딩 파일입니다. 이 경우 JSON Machine이 스칼라 값을 PHP 스트림으로 생성하는 것을 지원할 때까지는 여전히 운이 좋지 않을 것입니다.
composer require halaxa/json-machine
이 저장소를 복제하거나 다운로드하고 부트스트랩 파일에 다음을 추가하세요.
spl_autoload_register ( require ' /path/to/json-machine/src/autoloader.php ' );
이 저장소를 복제하세요. 이 라이브러리는 두 가지 개발 접근 방식을 지원합니다.
사용 가능한 개발 스크립트를 보려면 프로젝트 디렉토리에서 composer run -l
실행하세요. 이렇게 하면 테스트와 같은 빌드 프로세스의 일부 단계를 실행할 수 있습니다.
Docker를 설치하고 호스트 시스템의 프로젝트 디렉토리에서 make
실행하여 사용 가능한 개발 도구/명령을 확인하세요. 빌드 프로세스의 모든 단계를 개별적으로 실행할 수도 있고 전체 빌드 프로세스를 한 번에 실행할 수도 있습니다. Make는 기본적으로 백그라운드의 컨테이너 내에서 Composer 개발 스크립트를 실행합니다.
make build
: 전체 빌드를 실행합니다. 동일한 명령이 GitHub Actions CI를 통해 실행됩니다.
이 도서관이 마음에 드시나요? 별표 표시하고, 공유하고, 보여주세요 :) 이슈와 끌어오기 요청은 매우 환영합니다.
아파치 2.0
톱니바퀴 요소: www.flaicon.com에서 TutsPlus가 만든 아이콘은 CC 3.0 BY에 의해 라이센스가 부여됩니다.
markdown-toc으로 생성된 목차