[์†๋‹ฅ์†๋‹ฅ] ๐Ÿš€ ์šฐ๋‹นํƒ•ํƒ• Spring REST Docs ์ ์šฉ๊ธฐ

ํ—Œ์น˜ยท2022๋…„ 8์›” 7์ผ
4

์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค

๋ชฉ๋ก ๋ณด๊ธฐ
20/30
post-thumbnail

ํ•ด๋‹น ๊ธ€์€ ์†๋‹ฅ์†๋‹ฅ ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ์— ์ž‘์„ฑ๋œ ๊ธ€๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์†๋‹ฅ์†๋‹ฅ ๋งํฌ

0. intro

์ €ํฌ ์†๋‹ฅ์†๋‹ฅ ํŒ€์€ ๊ธฐ์กด์—” API ๋ช…์„ธ๋ฅผ notion์„ ํ†ตํ•ด ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์‹ค์ œ ์ฝ”๋“œ์™€ ์—ฐ๋™๋˜์ง€ ์•Š๋Š” ์ ์ด ๋ถˆํŽธํ–ˆ๊ณ , API๋ณ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•œ ๊ฒ€์ฆ์„ ํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด์— ์‹ ๋ขฐ์„ฑ์žˆ๋Š” API ๋ฌธ์„œ๋ฅผ ํ”„๋ก ํŠธ์—”๋“œ์— ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” REST Docs๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.
๋‹ค์Œ์€ REST Docs ๋ฅผ ์„ธํŒ…ํ•˜๊ณ  ๋ฌธ์„œํ™” ํ•œ ๊ณผ์ •์ž…๋‹ˆ๋‹ค.

1. build.gradle

plugins {
   id 'org.springframework.boot' version '2.7.1'
   id 'io.spring.dependency-management' version '1.0.11.RELEASE'
   id 'java'
   id "org.asciidoctor.jvm.convert" version "3.3.2"
}

group = 'com.wooteco'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
   asciidoctorExtensions // dependencies ์—์„œ ์ ์šฉํ•œ ๊ฒƒ ์ถ”๊ฐ€
}

repositories {
   mavenCentral()
}
ext {
	// ์‚ฌ์šฉํ•  ๋ณ€์ˆ˜ ์„ ์–ธ
   set('snippetsDir', file("build/generated-snippets"))
}

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
   implementation 'org.springframework.boot:spring-boot-starter-validation'
   implementation 'org.springframework.boot:spring-boot-starter-web'
   implementation 'org.springframework.boot:spring-boot-starter-mail'
   implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3'
   implementation 'com.google.guava:guava:31.0.1-jre'

		// build/generated-snippets ์— ์ƒ๊ธด .adoc ์กฐ๊ฐ๋“ค์„ ํ”„๋กœ์ ํŠธ ๋‚ด์˜ .adoc ํŒŒ์ผ์—์„œ ์ฝ์–ด๋“ค์ผ ์ˆ˜ ์žˆ๋„๋ก ์—ฐ๋™
    // .adoc ํŒŒ์ผ์„ HTML๋กœ ๋งŒ๋“ค์–ด export
   asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
   compileOnly 'org.projectlombok:lombok'

   runtimeOnly 'com.h2database:h2'
   runtimeOnly 'mysql:mysql-connector-java'

   annotationProcessor 'org.projectlombok:lombok'

   testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

   testImplementation 'org.springframework.boot:spring-boot-starter-test'
   testImplementation 'io.rest-assured:rest-assured:4.4.0'
   testImplementation 'io.rest-assured:spring-mock-mvc:4.4.0'
   testImplementation 'org.mockito:mockito-inline:3.8.0'

   implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
   runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
   runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

test {
   // ์œ„์—์„œ ์ž‘์„ฑํ•œ snippetsDir ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ test์˜ output์œผ๋กœ ๊ตฌ์„ฑํ•˜๋Š” ์„ค์ • 
   // -> ์Šค๋‹ˆํŽซ ์กฐ๊ฐ๋“ค์ด build/generated-snippets๋กœ ์ถœ๋ ฅ
   outputs.dir snippetsDir

   // ํ…Œ์ŠคํŠธ ์‹คํ–‰
   useJUnitPlatform()
}

// asciidoctor ์ž‘์—… ๊ตฌ์„ฑ
asciidoctor {
   // ์œ„์—์„œ ์ž‘์„ฑํ•œ configuration ์ ์šฉ
   configurations 'asciidoctorExtensions'
   
   // source๊ฐ€ ์—†์œผ๋ฉด .adocํŒŒ์ผ์„ ์ „๋ถ€ html๋กœ ๋งŒ๋“ค์–ด๋ฒ„๋ฆผ
   // source ์ง€์ •์‹œ ํŠน์ • adoc๋งŒ HTML๋กœ ๋งŒ๋“ ๋‹ค.
   sources{
      include("**/index.adoc","**/common/*.adoc")
   }

   // ํŠน์ • .adoc์— ๋‹ค๋ฅธ adoc ํŒŒ์ผ์„ ๊ฐ€์ ธ์™€์„œ(include) ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์„ ๊ฒฝ์šฐ ๊ฒฝ๋กœ๋ฅผ baseDir๋กœ ๋งž์ถฐ์ฃผ๋Š” ์„ค์ •
   // (๊ฐœ๋ณ„ adoc์œผ๋กœ ์šด์˜ํ•œ๋‹ค๋ฉด ํ•„์š” ์—†๋Š” ์˜ต์…˜)
   baseDirFollowsSourceFile()

   // snippetsDir ๋ฅผ ์ž…๋ ฅ์œผ๋กœ ๊ตฌ์„ฑ
   inputs.dir snippetsDir

   // asciidoctor ์ „ test ์‹คํ–‰!
   dependsOn test
}

// static/docs ํด๋” ๋น„์šฐ๊ธฐ
asciidoctor.doFirst {
   delete file('src/main/resources/static/docs')
}

// asccidoctor ์ž‘์—… ์ดํ›„ ์ƒ์„ฑ๋œ HTML ํŒŒ์ผ์„ static/docs ๋กœ copy
task createDocument(type: Copy) {
   dependsOn asciidoctor

   from file("build/docs/asciidoc")
   into file("src/main/resources/static")
}

// build ์ „ createDocument(REST Docs ๋ฌธ์„œํ™”) ์‹คํ–‰!
build {
   dependsOn createDocument
}

Gradle 7.4.1 ๋ฒ„์ „

grovvy ๋ฌธ๋ฒ•์„ ์ •ํ™•ํžˆ ์•Œ์ง€ ๋ชปํ•ด ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1-1. ๋นŒ๋“œ ๊ณผ์ •

  1. ํ…Œ์ŠคํŠธ ์‹คํ–‰
  2. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ build/generated-snippets์— ์ €์žฅ
  3. ์ด์ „ static/index.html ๋น„์šฐ๊ธฐ
  4. src/docs/asciidoc/index.adoc์„ ํ†ตํ•ด build/docs/asciidoc/ ์— index.html ์ƒ์„ฑ
  5. ์ƒ์„ฑ๋œ HTML ํŒŒ์ผ์„ build ์—์„œ src/main/resources/static/๋กœ ์ด๋™ํ•œ๋‹ค

์‚ฌ์‹ค ์ด๋•Œ index.html ์ด ์ƒ์„ฑ๋˜๋Š” ์ฃผ๊ธฐ๋ฅผ ์–ธ์ œ๋กœ ํ• ์ง€ ๊ณ ๋ฏผํ–ˆ์Šต๋‹ˆ๋‹ค.

build ๋‹จ์œ„๋กœ ์žฌ์ƒ์„ฑ์„ ํ•  ์‹œ build ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์—, test ๋งˆ๋‹ค ์‹คํ–‰ ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง„๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

asciidoctor ๋ช…๋ น์–ด๋ฅผ ์ด์šฉํ•ด ์ˆ˜๋™ export ๋ฅผ ํ•˜๋Š” ๋ฐฉ์•ˆ๋„ ๊ณ ๋ คํ–ˆ์ง€๋งŒ, ์•„์ง ์‹คํ–‰ ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง„ ์ •๋„๊ฐ€ ํฌ์ง€ ์•Š์•„ ํ˜„์žฌ ์„ธํŒ…์„ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

2. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

2-1. RestAssuredMockMvc

Spring์˜ย MockMvcย ์œ„์— ๋นŒ๋“œ๋œ REST API์ธ RestAssuredMockMvc ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

RestAssured ์™€ ๋ฌธ๋ฒ•์ด ๊ฐ™๊ณ , @WebMvcTest ํ˜•ํƒœ์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

ํŽธํ•œ ๋ฌธ๋ฒ•์œผ๋กœ ๋น ๋ฅด๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ํ•ด๋‹น API๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ด€๋ จ ๋งํฌ

๊ณต์‹๋ฌธ์„œ

RestAssuredMockMvc - spring-mock-mvc 2.8.0 javadoc

baeldung

2-2. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ธํŒ… (ControllerTest)

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;

// ... ๋“ฑ๋ก๋œ ์ปจํŠธ๋กค๋Ÿฌ/ MockBean ์ž„ํฌํŠธ

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@WebMvcTest({
        PostController.class,
        // ... ํ…Œ์ŠคํŠธํ•  ์ปจํŠธ๋กค๋Ÿฌ ๋“ฑ๋ก
})
@ExtendWith(RestDocumentationExtension.class)
public class ControllerTest {

    protected MockMvcRequestSpecification restDocs;

    @MockBean
    protected PostService postService;
    // ... MockBean ๋“ฑ๋ก

		// RestDocs ์„ธํŒ…
    @BeforeEach
    void setRestDocs(WebApplicationContext webApplicationContext,
                     RestDocumentationContextProvider restDocumentation) {

        restDocs = RestAssuredMockMvc.given()
                .mockMvc(MockMvcBuilders.webAppContextSetup(webApplicationContext)
                        .apply(documentationConfiguration(restDocumentation)
                                .operationPreprocessors()
                                .withRequestDefaults(prettyPrint())
                                .withResponseDefaults(prettyPrint()))
                        .build())
                .log().all();
    }
}

2-3. ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๋ฐฉ์‹

class HashtagControllerTest extends ControllerTest { [// (0)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)

    @DisplayName("ํ•ด์‹œํƒœ๊ทธ๋กœ ๊ฒ€์ƒ‰ ์‹œ ์—†๋Š” ํ•ด์‹œํƒœ๊ทธ์ด๋ฉด 404 ๋ฐ˜ํ™˜")
    @Test
    void findPostsWithHashtags_Exception_NoHashtag() {
				// Mockito๋กœ ๋“ฑ๋ก๋œ MockBean ์„ธํŒ…
        doThrow(new HashtagNotFoundException())
                .when(hashtagService)// [(1)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)
                .findPostsWithHashtag(matches("์—†๋Š”ํƒœ๊ทธ"), any());

				// ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰
        restDocs
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .when().get("/posts?hashtag=์—†๋Š”ํƒœ๊ทธ&size=5&page=0")
                .then().log().all()
                .assertThat() [// (2)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)
                .apply(document("search/byHashtag/fail/noHashtag")) [// (3)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)
                .statusCode(HttpStatus.NOT_FOUND.value());
    }

(0) ์œ„์˜ ControllerTest ๋ฅผ ์ƒ์†ํ•ฉ๋‹ˆ๋‹ค.
(1) hashtagService๋Š” ControllerTest ์— @MockBean์œผ๋กœ ๋“ฑ๋ก๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
(2) ๊ฐ€๋…์„ฑ์„ ๋•๋Š” ์ฝ”๋“œ. ์•„๋ž˜ ๋‚ด์šฉ๋“ค์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
(3) build/docs/asciidoc/ ์•ˆ์˜ ํ•ด๋‹น ๋””๋ ‰ํ† ๋ฆฌ์— ๊ฒฐ๊ณผ adoc ํŒŒ์ผ๋“ค์ด ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

3. AsciiDocs ์ž‘์„ฑ

3-1. adoc ์กฐ๊ฐ๋“ค ์ƒ์„ฑ

๋นŒ๋“œํ•˜๋ฉด build/generated-snippets ํด๋”์— ํ…Œ์ŠคํŠธ์—์„œ ์ž‘์„ฑํ•œ board/create/fail ๋””๋ ‰ํ† ๋ฆฌ์— adoc ์กฐ๊ฐ๋“ค์ด ์ƒ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์กฐ๊ฐ๋“ค์ด default๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

  • curl-request.adoc
  • http-request.adoc
  • httpie-request.adoc
  • http-response.adoc
  • request body
  • response body

์šฐ๋ฆฌ๋Š” ์ด ์ค‘ http-request.adoc (์š”์ฒญ) , http-response.adoc (์‘๋‹ต)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด์™ธ ํŒŒ์ผ๋“ค์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

3-2. index.adoc ๋ฌธ์„œ ์ž‘์„ฑ

1) ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์น˜

์šฐ์„  adoc ํŒŒ์ผ ์ž‘์„ฑ์˜ ํŽธ์˜๋ฅผ ์œ„ํ•ด AsciiDoc ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๋‹น ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ adoc ๊ฒฐ๊ณผ๋ฌผ์„ gui ํ˜•ํƒœ๋กœ ๋ณผ ์ˆ˜ ์žˆ๊ณ , ๋ฌธ๋ฒ• ํ™•์ธ๋„ ๋” ๊ฐ„ํŽธํ•ด์ง‘๋‹ˆ๋‹ค!

2) ๋ฌธ์„œ ์ž‘์„ฑ

์ด์ œ API ๋ฌธ์„œ๋ฅผ adoc ํ˜•ํƒœ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

src/docs/asciidoc/ ๋””๋ ‰ํ† ๋ฆฌ ์•ˆ์— index.adoc ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

(๋…ธ๊ฐ€๋‹คโ€ฆ)

= ์†๋‹ฅ์†๋‹ฅ API ๋ช…์„ธ
:doctype: book
:icons: font
:source-highlighter: highlightjs // ์ฝ”๋“œ๋“ค์˜ ํ•˜์ด๋ผ์ดํŒ…์„ highlightjs๋ฅผ ์‚ฌ์šฉ
:toc: left // Table Of Contents(๋ชฉ์ฐจ)๋ฅผ ๋ฌธ์„œ์˜ ์ขŒ์ธก์— ๋‘๊ธฐ
:toclevels: 2 // ๋ชฉ์ฐจ ๋ ˆ๋ฒจ ์„ค์ •
:sectlinks:
:sectnums: // ๋ถ„๋ฅ˜๋ณ„ ์ž๋™์œผ๋กœ ์ˆซ์ž๋ฅผ ๋‹ฌ์•„์คŒ 
:docinfo: shared-head

== ๋ฉ”์ธํŽ˜์ด์ง€ // "="๋Š” ๋งˆํฌ๋‹ค์šด "#"๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ

=== ๋ฉ”์ธํŽ˜์ด์ง€ ์กฐํšŒ
==== ์„ฑ๊ณต
operation::board/find/content[snippets='http-request,http-response'] // ํŠน์ • adoc ํŒŒ์ผ ์ž๋™ ์ž„ํฌํŠธ

//... ์•„๋ž˜ ๋ถ€๋ถ„๋“ค ์ง์ ‘ ์ž‘์„ฑ
  • operation::๋””๋ ‰ํ† ๋ฆฌ๋ช…[snippets=โ€™์›ํ•˜๋Š” ์กฐ๊ฐ๋“คโ€™]
    • ๋ฌธ์„œ๋กœ ์‚ฌ์šฉํ•  ์กฐ๊ฐ๋“ค์„ ๋ช…์‹œํ•ด ์ž๋™์œผ๋กœ ๊ฐ€์ ธ์˜จ๋‹ค.

์ž์„ธํ•œ Asciidoc ์‚ฌ์šฉ๋ฒ• ๋งํฌ

3-3. ๋นŒ๋“œ ํ›„ index.html ์ƒ์„ฑ

์ด์ œ ๋นŒ๋“œํ•˜๋ฉด

์œ„์— ์ž‘์„ฑํ•œ index.adoc ํŒŒ์ผ์ด index.html ํ˜•ํƒœ๋กœ export๋ฉ๋‹ˆ๋‹ค!
index.html ํŒŒ์ผ์€
build/docs/asciidoc/ , src/main/resources/static/ ๋‘ ์นดํ…Œ๊ณ ๋ฆฌ ์•ˆ์— ์žˆ์Šต๋‹ˆ๋‹ค.

( build.gradle ์˜ createDocument ๋ช…๋ น์–ด๋กœ ๋ณต์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค.)

์ด์ค‘ ์ €ํฌ๊ฐ€ ์›น ์ƒ์— ๋ณด์—ฌ์ค„ ๋ฌธ์„œ๋Š” src/main/resources/static/ ์•ˆ์— ์œ„์น˜ํ•ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์„œ ๋งํฌ

1) ๊นƒํ—ˆ๋ธŒ push

์ด์ œ src/main/resources/static/index.html ํŒŒ์ผ์„ pr์„ ๋‚ ๋ ค
๊นƒํ—ˆ๋ธŒ ๋ ˆํฌ์— ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

2) README.md์— ๋งํฌ ์ถ”๊ฐ€

## ์†๋‹ฅ์†๋‹ฅ

[RESTDocs ๋กœ ๊ตฌํ˜„๋œ API ๋ช…์„ธ](backend/sokdak/src/main/resources/static/index.html)

README.md์— ๋งํฌ๋ฅผ ๋‚จ๊ฒจ์ฃผ์‹œ๋ฉด

๋‹ค์Œ๊ณผ ๊ฐ™์ด index.html์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

3) github pages ์„ค์ •

์ข€๋” ํŽธํ•˜๊ฒŒ API ๋ฌธ์„œ๋ฅผ ๋ณด๊ธฐ ์œ„ํ•ด github pages๋ฅผ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.

์ด๋•Œ

  • main ๋ธŒ๋žœ์น˜๋กœ ํ™œ์„ฑํ™”ํ•˜๋ฉด main ๋ธŒ๋žœ์น˜ ์† index.html๋กœ
  • dev ๋ธŒ๋žœ์น˜๋กœ ํ™œ์„ฑํ™”ํ•˜๋ฉด dev ๋ธŒ๋žœ์น˜ ์† index.html๋กœ

์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ๋Š” dev ๋ธŒ๋žœ์น˜๋กœ ์„ค์ •ํ•ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ

์ด์ œ https://woowacourse-teams.github.io/2022-sokdak/ ๋กœ ๋“ค์–ด๊ฐˆ ์‹œ

README.md ๊ฐ€ ๋œจ๊ณ 
๋งํฌ๋ฅผ ํด๋ฆญ ์‹œ

์–ธ์ œ ์–ด๋””์„œ๋‚˜ ํ•ด๋‹น ๋งํฌ๋กœ API ๋ช…์„ธ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ฐœ๋ฐœ์ž ๋ฟ ์•„๋‹ˆ๋ผ ์œ ์ €๋“ค๋„ ํ•ด๋‹น API ๋ฌธ์„œ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

profile
๐ŸŒฑ ํ•จ๊ป˜ ์ž๋ผ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค ๐Ÿš€ rerub0831@gmail.com

0๊ฐœ์˜ ๋Œ“๊ธ€