Svelte 애플리케이션을 배포하는 방법?

How to Deploy an Svelte Application_
How to Deploy an Svelte Application_

이 문서에서는 Svelte를 사용하여 웹 애플리케이션을 빌드하고 Back4app 컨테이너를 사용하여 배포하는 방법에 대해 알아봅니다.

Svelte는 반응형 및 동적 웹 애플리케이션을 개발하기 위한 JavaScript 프레임워크입니다. Svelte 프레임워크를 사용하면 가볍고 고성능 애플리케이션을 빌드할 수 있습니다. 기존 JavaScript 프레임워크와 달리 Svelte는 브라우저에서 빌드 단계로 많은 무거운 작업을 옮기기 때문입니다.

프로젝트 개요: 재고 추적기

이 글에서는 Back4App의 실시간 데이터베이스와 통합되는 인벤토리 트래커를 소개합니다.

이 애플리케이션은 사용자의 인벤토리 데이터를 관리하여 필요에 따라 제품 정보를 추가, 검색, 삭제할 수 있도록 합니다.

트래커는 제품 이름, 가격, 수량과 같은 필수 세부 정보를 Back4App에 저장합니다.

이를 통해 사용자는 재고를 쉽게 관리하고 모니터링하여 제품에 대한 정확한 최신 정보를 확보할 수 있습니다.

Svelte 애플리케이션 만들기

이 섹션에서는 Vite(프런트엔드 프레임워크 빌드 도구)를 사용하여 Svelte 프로젝트를 생성합니다.

터미널에서 다음 명령을 실행하여 Vite로 Svelte 애플리케이션을 만들 수 있습니다:

npm init vite

이 명령을 실행한 후 프로젝트의 이름을 입력하고 프레임워크(Svelte)를 선택한 다음 프레임워크에 사용할 언어 변형을 선택합니다.

이렇게요:

Vite CLI

위 이미지에서는 Svelte 프로젝트의 이름이 인벤토리 추적기이고 언어 변형이 JavaScript임을 알 수 있습니다.

다음으로, Svelte 프로젝트에 필요한 몇 가지 종속성을 설치해야 합니다. 종속 요소를 설치하려면 프로젝트 디렉터리로 전환하여 아래 명령을 실행합니다:

# Switch to the project directory
cd inventory-tracker

# Install dependencies
npm install

이 명령은 프로젝트에 필요한 모든 종속성을 설치하며, 이제 IDE에서 애플리케이션 빌드를 시작할 수 있습니다.

Svelte 애플리케이션 구축

이 섹션에서는 Svelte와 Back4app의 서비스형 백엔드 기능을 사용하여 인벤토리 트래커 애플리케이션을 구축합니다.

애플리케이션에는 데이터를 추가, 가져오기, 편집, 삭제할 수 있는 CRUD(생성, 읽기, 업데이트, 삭제) 기능이 있습니다.

Svelte 애플리케이션 빌드를 시작하기 전에 svelte 라우팅 라이브러리를 설치해야 합니다.

Svelte 라우팅 라이브러리는 Svelte 애플리케이션에 라우팅 기능을 추가하는 라이브러리로, 단일 페이지 애플리케이션(SPA)을 만들 수 있습니다.

아래 명령을 실행하여 Svelte 라우팅 라이브러리를 설치합니다:

npm i -D svelte-routing

설치가 완료되면 프로젝트의 src 디렉터리에 AddProduct와 Home 컴포넌트를 생성합니다. AddProduct 컴포넌트에 다음 코드 줄을 추가합니다:

<!-- AppProduct.svelte -->
<script>
	let product = {
	  name: "",
	  quantity: "",
	  price: "",
	}
</script>

<form>
	<input type="text" placeholder="Name of Product" bind:value={product.name}>
  <input type="number" placeholder="No of Products" bind:value={product.quantity}>
  <input type="number" placeholder="Price of Products" bind:value={product.price}>
	
	<div>
		<button>Add Product</button>
	</div>
</form>

<style>
	form{
		display: flex;
		flex-direction: column;
		gap: 2rem;
		align-items: center;
	}
</style>

위의 코드 블록은 제품 세부 정보를 받는 양식을 렌더링합니다. 이 양식에는 텍스트(이름)숫자(수량가격)의 세 가지 입력 요소가 포함되어 있습니다.

입력 필드에는 제품 이름, 사용 가능한 제품 수, 제품 가격이 입력됩니다.

입력 요소의 bind:value 속성을 사용하면 코드 블록이 입력 값을 지정된 제품 개체 속성에 바인딩합니다. 스타일 섹션에는 이 Svelte 컴포넌트로 범위가 지정된 CSS 스타일이 포함되어 있습니다.

그런 다음 아래 코드 블록을 컴포넌트에 추가합니다:

<!-- Home.svelte -->
<script>
    import {Link} from "svelte-routing";
</script>

<main>
    <p>A way to manage and keep track of products in your inventory</p>
    <Link to="/add-products" class="link">Add Products here →</Link>
</main>

<style>
    main{
    display: flex;
    flex-direction: column;
    gap: 2rem;
    align-items: center;
  }
</style>

구성 요소는 간결한 Svelte-라우팅 라이브러리에서 링크 구성 요소를 가져옵니다. Link 컴포넌트는 사용자를 “/add-products” 경로로 안내합니다. Link 컴포넌트가 실제로 작동하도록 하려면 이 경로를 정의해야 합니다.

경로를 정의하려면 컴포넌트를 열고 아래 코드 블록을 추가합니다:

<!-- App.svelte -->
<script>
  import {Route, Router} from "svelte-routing";
  import AddProduct from './AddProduct.svelte';
  import Home from './Home.svelte';

  export let url = "";
</script>

<Router {url}>
  <h1>Inventory Tracker</h1>
  
  <div class="container">
    <Route path="/" component={Home} />
    <Route path="/add-products" component={AddProduct} /> 
  </div>
</Router>

<style>
  h1{
    text-align: center;
    font-family: "Poppins", sans-serif;
    margin-block-start: 1rem;
    margin-block-end: 6rem;
  }
</style>

위의 코드 블록은 경로라우터 구성 요소와 제품 추가 구성 요소를 Svelte 라우팅에서 가져와 개별 경로를 정의합니다.

경로 구성 요소를 사용하면 애플리케이션에서 다양한 경로를 정의할 수 있습니다. 이 경우 제품 추가 경로입니다.

라우터 컴포넌트 내에서 HTML 섹션을 래핑하면 동봉된 컴포넌트에 대한 라우팅이 초기화됩니다.

위의 코드 블록에서 애플리케이션을 렌더링하면 경로의 경로가 “/”이므로 경로가 먼저 표시됩니다.

다음 단계는 애플리케이션의 글로벌 스타일을 정의하는 것입니다. 이렇게 하려면 src 디렉터리 내에 styles 폴더를 만듭니다. styles 폴더에 global.css 파일을 추가하고 이 파일에서 애플리케이션의 전역 스타일을 정의합니다.

아래 코드 블록을 global.css 파일에 추가합니다:

/* global.css */
@import url('<https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap>');
@import url('<https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap>');

*{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body{
    font-family: "Montserrat", sans-serif;
    background-color: #1F2124;
    color: #FFFFFF;
}

.container{
    inline-size: 60%;
    margin: auto;
}

.link{
    text-decoration: none;
    color: inherit;
    font-weight: 500;
}

.link:hover{
    color: #99BCF6;
}

input{
    padding: 1rem;
    border-radius: 12px;
    outline: none;
    border: none;
    font-family: "Montserrat", sans-serif;
    color: #333333;
    inline-size: 100%;
}

button{
    padding: 0.7rem 1rem;
    border-radius: 10px;
    font-weight: 500;
    background-color: #FFFFFF;
    font-family: "Montserrat", sans-serif;
}

button:hover{
    background-color: #99BCF6;
}

스타일을 정의한 후 App 컴포넌트에서 global.css 파일을 가져와서 정의된 스타일을 애플리케이션에 적용합니다. 이 코드를 컴포넌트의 스크립트 섹션에 추가하면 됩니다:

//App.svelte
import './styles/global.css';

이제 Svelte 애플리케이션을 빌드했습니다. 다음으로 Back4app을 사용하여 애플리케이션의 백엔드를 빌드합니다.

Back4app 애플리케이션 설정

이 섹션에서는 Back4app AI 에이전트를 사용하여 애플리케이션의 백엔드 역할을 하는 Back4app 애플리케이션을 생성합니다.

Back4app 계정을 만들려면 Back4app 계정이 필요합니다. 계정이 없는 경우 무료로 계정을 만들 수 있습니다.

계정에 로그인하고 Back4app 계정 대시보드의 탐색 모음에서 ‘AI 에이전트 ‘ 링크를 클릭합니다.

Back4app 앱 페이지

AI 에이전트에 액세스할 수 있게 되면 AI 에이전트에게 애플리케이션 생성을 요청하는 메시지를 입력합니다.

프롬프트는 아래와 비슷하게 표시됩니다:

Create a new application named inventory-tracker

위의 프롬프트에서 볼 수 있듯이 애플리케이션의 이름을 지정해야 합니다. AI 에이전트가 애플리케이션 생성을 완료하는 즉시 애플리케이션 생성을 확인하는 응답을 보냅니다.

응답에는 아래 이미지의 응답과 유사하게 애플리케이션의 자격 증명도 포함되어야 합니다.

AI 상담원 응답

AI 에이전트가 제공하는 다양한 자격 증명 중 앱 ID와 JavaScript 키를 복사해야 합니다. Svelte 애플리케이션을 Back4app 애플리케이션과 연결하려면 이 두 가지가 필요합니다.

다음으로 Back4app 애플리케이션에서 인벤토리 클래스를 생성합니다. 해당 클래스에서 이름, 수량 및 가격 열을 추가합니다. 이렇게 하려면 AI 에이전트를 사용하여 다음 프롬프트를 작성합니다:

In the inventory-tracker app, create an inventory class and add a name, price, and quantity column to the class.

아래 이미지와 유사한 응답이 표시될 것입니다.

AI 상담원 응답

이제 Svelte UI Back4app 백엔드가 준비되었으므로 UI를 백엔드에 연결합니다.

Svelte 애플리케이션을 Back4app에 연결하기

이 섹션에서는 Svelte 애플리케이션을 Back4app 애플리케이션에 연결합니다. 이렇게 하려면 Parse SDK가 필요합니다.

Parse SDK는 웹 애플리케이션에서 사용할 수 있는 백엔드 서비스를 제공하는 개발 도구 세트입니다.

아래 명령을 실행하여 Parse SDK를 설치합니다:

npm install parse

SDK를 설치한 후 App.svelte 파일의 스크립트 태그에 아래 코드 블록에 코드를 추가합니다.

import Parse from 'parse/dist/parse.min.js';

Parse.initialize('YOUR_APP_ID', 'YOUR_JAVASCRIPT_KEY');
Parse.serverURL = '<https://parseapi.back4app.com/>';

'YOUR_APPLICATION_ID ‘와 'YOUR_CLIENT_KEY ‘를 앞서 복사한 자격 증명으로 바꿉니다. 환경 변수를 사용하여 안전하게 저장해야 합니다.

Back4app에 데이터 추가

Back4app에 데이터를 추가하려면 AddProduct 컴포넌트 양식의 입력 값을 사용합니다. 사용자가 제출한 값을 가져와서 Back4app의 데이터베이스에 추가합니다.

AddProduct 컴포넌트의 스크립트 섹션에서 addData 함수를 생성합니다. 이 함수에는 Back4app에 제품 세부 정보를 추가하는 로직이 포함됩니다.

이렇게요:

// AddProduct.svelte
import Parse from 'parse/dist/parse.min.js';
import { navigate } from "svelte-routing";

let addData = (event) => {
  event.preventDefault();
  try {
    const Inventory = new Parse.Object("Inventory");
    Inventory.set("name", product.name);
    Inventory.set("quantity", +product.quantity);
    Inventory.set("price", +product.price);
    Inventory.save().then(() => {
      console.log("New Product added successfully");
      navigate("/", { replace: true });
    });
  } catch (error) {
    console.log(error);
  }
};

위의 코드 블록에서 addData 함수는 Inventory 클래스에 대한 새 Parse 객체 Inventory를 생성합니다.

개체를 데이터베이스에 저장하기 전에 이름, 수량가격 필드의 값을 제품 속성의 해당 값으로 설정합니다.

제품.수량제품.가격 속성 앞에 단항 더하기(+) 연산자가 있다는 점에 유의하세요.

연산자는 속성을 숫자 유형으로 변환합니다. 제출 이벤트 핸들러를 사용하여 AddProduct 컴포넌트의 폼에 addData 함수를 바인딩합니다.

이렇게 하면 사용자가 양식을 제출할 때마다 추가 데이터 함수가 트리거됩니다.

제출 이벤트 핸들러를 사용하여 함수를 양식에 바인딩하려면 AddProduct 컴포넌트의 양식을 아래 양식으로 바꾸세요:

<!--AddProduct.svelte-->
<form on:submit={addData}>
	<input type="text" placeholder="Name of Product" bind:value={product.name}>
	<input type="number" placeholder="No of Products" bind:value={product.quantity}>
	<input type="number" placeholder="Price of Products" bind:value={product.price}>
	
	<div>
	    <button>Add Product</button>
	</div>
</form>

Back4app에서 데이터 가져오기

Back4app에서 데이터를 가져오려면 이전 섹션에서 Back4app 애플리케이션에 저장된 개체 구문 분석에 액세스하여 개체에서 값을 가져옵니다.

데이터를 가져오기 전에 애플리케이션의 src 디렉토리에 카드 컴포넌트를 만듭니다. 이 컴포넌트는 Back4app에서 가져온 데이터의 모양과 느낌을 결정합니다.

컴포넌트 파일에 다음 코드를 작성합니다:

<!-- Card.svelte -->
<script>
	export let name = '';
	export let quantity = 0;
	export let price = 0;
</script>

<div class="card">
	<h3>{name}</h3>
	<div class="details">
	  <p>Price: ${price}</p>
	  <p>Quantity: {quantity == 0 ? "out of stock" : quantity}</p>
	</div>
	<div>
	  <button>Delete</button>
	</div>
</div>

<style>
	.card{
	    display: flex;
	    flex-direction: column;
	    gap: 1.9rem;
	    padding: 1rem;
	    border-radius: 12px;
	    background-color: #e2e2e2;
	    color: #1F2124;;
	    inline-size: 100%;
	}
	
	.details{
	    display: flex;
	    gap: 3rem;
	}
	
	.details p{
	    font-size: 14px;
	    font-weight: 500;
	    color: #888888;
	}
</style>

카드 컴포넌트는 제품의 이름, 수량, 가격을 표시합니다. 위 코드 블록의 세 가지 프로퍼티인 이름, 수량, 가격을 사용하여 상위 컴포넌트에서 이러한 값을 가져옵니다.

이제 컴포넌트의 스크립트 태그에 아래 코드 블록에 코드를 추가합니다:

//Home.svelte
import { onMount } from "svelte";
import Parse from "parse/dist/parse.min.js";

let products = [];

const fetchProducts = async () => {
  try {
    const query = new Parse.Query("Inventory");
    const productsData = await query.find();
    products = productsData;
  } catch (error) {
    console.log(error);
  }
};

onMount(fetchProducts);

이 코드는 Svelte 프레임워크에서 onMount 수명 주기 함수를 가져옵니다. 또한 처음에는 비어 있는 제품 배열을 생성합니다.

코드 블록에서 Back4app에서 필요한 데이터를 가져오는 로직을 담당하는 fetchProducts 함수를 찾을 수 있습니다.

fetchProducts 함수는 Parse.Query 메서드를 사용하여 앱의 데이터베이스에서 “인벤토리” 객체를 검색합니다.

그런 다음 쿼리에서 find() 메서드를 호출하여 쿼리 결과의 배열을 반환합니다. 마지막으로 결과 배열을 제품 변수에 할당합니다.

fetchProducts 함수를 onMount 함수의 인수로 설정하면 컴포넌트를 렌더링할 때마다 애플리케이션이 데이터를 가져오도록 할 수 있습니다.

구성 요소의 HTML 섹션에서 제품 배열의 데이터를 표시합니다.

이렇게요:

<!-- Home.svelte-->
<div class="products">
	{#each products as product}
		<Card name={product.get('name')} quantity={product.get('quantity')} price={product.get('price')}/>
	{/each}
</div>

블록은 제품 배열을 반복하고 배열의 각 제품에 대한 카드 구성 요소를 표시합니다.

카드 컴포넌트는 제품의 get 메서드를 사용하여 이름, 수량 및 제품 가격의 값을 가져옵니다. 그런 다음 해당 값을 소품에 할당합니다.

컴포넌트의 스타일 태그에 아래 정의된 스타일을 추가하여 블록을 감싸는 div 태그의 스타일을 지정합니다.

/* Home.svelte */
.products{
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 3rem;
	border-top: 2px solid #e2e2e2;
	margin-block-start: 3rem;
	padding-block-start: 2rem;
}

Back4app에서 데이터 삭제

Svelte 애플리케이션에 삭제 기능을 추가하려면 다음과 같이 하세요. 카드 컴포넌트부터 시작하여 컴포넌트를 수정해야 합니다. 카드 컴포넌트에서 핸들클릭이라는 새 프로퍼티를 생성합니다.

컴포넌트의 스크립트 섹션에 아래 코드 줄을 추가하여 소품을 만듭니다:

//Card.svelte
export let handleClick;

이제 클릭 이벤트 핸들러를 사용하여 컴포넌트의 HTML 섹션에 있는 버튼 요소에 소품을 바인딩합니다.

이렇게요:

<!-- Card.svelte -->
<div>
	<button on:click={handleClick}>Delete</button>
</div>

컴포넌트에서 삭제 제품 함수를 생성합니다. 이 함수에는 선택한 제품을 삭제하는 로직이 포함되어 있습니다.

컴포넌트의 스크립트 섹션에 아래 코드 줄을 추가합니다.

// Home.svelte
const deleteProduct = async (id) => {
  try {
    const Product = Parse.Object.extend("Inventory");
    const product = new Product();
    product.id = id;
    await product.destroy();
    const newData = products.filter((item) => item.id !== id);
    products = newData;
  } catch (error) {
    console.log(error);
  }
};

위의 코드 블록에서 deleteProduct 함수는 새 “Product” 객체를 생성하고 객체의 id 속성을 함수의 id 매개 변수로 설정한 다음 객체의 비동기 파괴 메서드를 호출하여 지정된 ID를 가진 제품을 삭제합니다.

이 함수는 제품 배열에서 지정된 ID를 가진 제품을 필터링하고 삭제된 제품을 제외한 새 배열을 생성합니다. 그런 다음 이 함수는 새 배열을 제품에 할당합니다.

다음으로 함수를 Card 컴포넌트의 handleClick 프로퍼티에 전달합니다. 이제 사용자가 Card 컴포넌트의 버튼을 클릭할 때마다 deleteProduct 함수가 트리거됩니다.

이렇게요:

<!-- Home.svelte -->
<Card 
	name={product.get('name')} 
	quantity={product.get('quantity')} 
	price={product.get('price')}
	handleClick={() => deleteProduct(product.id)}
/>

애플리케이션 테스트

애플리케이션이 제대로 작동하는지 확인하기 위해 애플리케이션을 테스트해야 합니다. 애플리케이션을 시작하려면 아래 명령을 실행하세요.

npm run dev

이 명령은 개발 서버에서 애플리케이션을 실행하고 웹 브라우저에서 애플리케이션을 볼 수 있도록 링크를 제공합니다.

링크를 클릭하면 아래 이미지와 같은 애플리케이션이 표시됩니다.

재고 추적기 홈페이지

‘여기에 제품 추가’ 링크를 클릭하면 다음과 같은 새 페이지로 리디렉션됩니다:

양식을 작성한 다음‘제품 추가‘ 버튼을 클릭하여 제출합니다.

이렇게 하면 제공한 세부 정보가 Back4app의 데이터베이스에 추가됩니다. Back4app 애플리케이션의 대시보드에서 이를 확인할 수 있습니다.

애플리케이션이 데이터를 성공적으로 추가하면 Back4app이 데이터베이스에 새 행을 추가합니다.

이렇게요:

Back4app 대시보드

양식을 제출하면 애플리케이션이 새 제품이 표시되는 홈 페이지로 리디렉션됩니다.

아이템이 있는 재고 추적기 홈 페이지

제품을 삭제하려면 제품 카드의 ‘삭제’ 버튼을 클릭하기만 하면 됩니다.

Svelte 애플리케이션 도커화

Back4app에 배포하려면 먼저 Svelte 애플리케이션을 도커화해야 합니다. Svelte 애플리케이션을 도커라이즈하려면 애플리케이션의 루트 디렉터리에 Dockerfile.dockerignore 파일을 생성합니다.

Docker파일에 다음 코드 줄을 작성합니다:

# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev"]

Docker 이미지를 빌드할 때 제외해야 하는 몇 가지 디렉터리는 다음과 같습니다. 이러한 디렉터리를 지정하려면 .dockerignore 파일에 해당 디렉터리를 추가하세요.

예를 들어

# .dockerignore
node_modules

위의 코드 블록은 이미지 빌드 프로세스 중에 컨텍스트에서 node_modules 디렉터리를 제외하도록 Docker에 지시합니다. Vite를 사용하여 Svelte 애플리케이션을 만들었으므로 Docker를 지원하도록 Vite를 구성해야 합니다.

이렇게 하려면 애플리케이션의 루트 디렉터리에 있는 vite.config.js 파일에 액세스합니다. 파일의 코드를 아래 코드 블록으로 바꿉니다:

import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

// <https://vitejs.dev/config/>
export default defineConfig({
  plugins: [svelte()],
  server: {
    host: true,
    strictPort: true,
    port: 5173,
   },
})

위의 코드 블록은 개발 서버가 수신 대기하는 포트를 지정하고 지정된 포트 이외의 포트에서 애플리케이션이 실행되지 않도록 합니다.

도커 이미지를 빌드하려면 터미널에서 다음 명령을 실행합니다:

docker build -t inventory-tracker .

Svelte 애플리케이션 배포

이제 애플리케이션을 도커화했으니 다음 단계는 Svelte 앱을 배포하는 방법입니다. Svelte 앱을 배포하려면 Back4app 컨테이너를 활용합니다.

애플리케이션을 배포하기 전에 Back4app이 해당 리포지토리를 사용하여 애플리케이션에 액세스할 수 있도록 애플리케이션을 GitHub 리포지토리에 푸시해야 합니다. Back4app에 GitHub 리포지토리에 대한 액세스 권한을 부여하려면 Back4app Github 앱을 사용하세요.

Back4app에 애플리케이션의 저장소에 대한 액세스 권한을 부여한 후, 이제 아래 프롬프트를 사용하여 AI 에이전트를 통해 Back4app에 애플리케이션을 배포할 수 있습니다:

Deploy my repository <<repository-url>> on Back4app containers

위의 프롬프트는 배포 프로세스를 시작합니다. < > 을 애플리케이션의 리포지토리 URL로 바꿔야 합니다.

배포가 성공하면 AI 에이전트가 애플리케이션의 배포 상태를 알려주고 배포에 대한 세부 정보를 제공하는 메시지를 사용자에게 보냅니다.

예를 들어

배포된 인벤토리 추적기 페이지

위 이미지는 애플리케이션이 성공적으로 배포되었으며 지정된 앱 URL을 방문하여 브라우저에서 애플리케이션에 액세스할 수 있음을 보여줍니다.

결론

이 글에서는 Back4pp를 사용하여 간단한 Svelte 애플리케이션을 빌드하는 방법을 배웠습니다. Back4app AI 에이전트를 사용하여 애플리케이션의 백엔드를 생성하고 Parse SDK를 사용하여 상호 작용했습니다.

또한 AI 에이전트는 Back4app 컨테이너에서 앱의 배포 프로세스를 간소화했습니다.

Back4app은 백엔드 및 배포 요구 사항을 관리하여 개발 워크플로우를 간소화합니다. 이를 통해 사용자가 좋아할 제품을 만드는 데 집중할 수 있습니다.

이 튜토리얼에 사용된 코드는 이 GitHub 리포지토리에서 확인할 수 있습니다.


Leave a reply

Your email address will not be published.