FARM 스택은 빠른 실행, 리액트, 还有什么 MongoDB를 결합한 现代化的 웹 開発 스택입니다. 이 전체 스택 솔루션은 개발자에게 확장性, 효율, 고 성능의 웹 응용 프로그램을 빌드하기 위한 강력한 도구 세트를 제공합니다.

이 記事에서는 FARM 스택과 Docker을 사용하여 项目을 만들고, 모든 것이 어떻게 함께 작동하는지 보실 수 있습니다.

이 記事는 저가 created freeCodeCamp.org YouTube 채널에서의 과정에 기반합니다. 여기서 봐보세요:

FARM 스택 소개

FARM 스택 中的 F 는 다음과 같습니다:

  • F: FastAPI (백 엔드)

  • R: React ( 前端)

  • M: MongoDB ( 데이터 베이스)

FARM 스택은 각 组建의 장점을 充分利用하기 위해 설계되었으며, 개발자는 기능 룰 적용 프로그램을 만들 수 있는 typify 개발 经验을 얻을 수 있습니다.

FARM 스택의 组建

  1. FastAPI: FastAPI는 现代化的, 고 성능의 파이썬 웹 프레임웍으로 API를 빌드하는 것을 도울 수 있습니다. 이를 사용하면 쉽고 빠르게 코드를 실행할 수 있고 생산 환경에 적합하게 만들 수 있습니다. FastAPI는 웹 部分을 Starlette를 이용하고 데이터 部分을 Pydantic를 이용하여 강력한 백 엔드 서비스를 만들 수 있는 좋은 선택입니다.

  2. React: React는 유저 인터페이스 빌드를 위한 인기 있는 JavaScript 라이브러리입니다. Facebook에 의해 개발되고 관리되며, 개발자는 데이터가 변경되는 것과 함께 재사용 가능한 UI 컴포넌트를 생성할 수 있습니다. 컴포넌트 기반 아키텍처와 가상 DOM이 있어 동적하고 반응형인 프론트 엔드 응용 프로그램을 빌드하는 것에 적절한 선택이 되어 있습니다.

  3. MongoDB: MongoDB는 文档型 NoSQL databases입니다. 靈活한 JSON-like 문서로 데이터를 저장하며, 문서의 필드가 다양하게 变化하고 데이터 구조가 시간 안에 변경되는 것을 의미합니다. 이러한 靈活성은 MongoDB가 quickly evolving appliactions과 다양한 데이터 형식을 처리하는 것에 적절한 선택이 되어 있습니다.

FARM Stack로 사용하는 장점

  1. 高性能: FastAPI는 가장 빠른 Python 프레임웍 중 하나로, React의 가상 DOM은 efficient UI update를 보장합니다. MongoDB의 문서 모델은 快速 읽기와 쓰기를 허용합니다.

  2. Scalability: FARM stack의 모든 组件가 스케일링 가능하며, FastAPI는 paralle 요청을 効率적으로 처리하며, React 응용 프로그램은 複雑한 UI를 관리하며, MongoDB은 다양한 서버에 数据的 distribution을 할 수 있습니다.

  3. Community and Ecosystem: 모든 三部 技術에 대해 large, active community가 있으며, rich ecosystem of libraries and tools를 갖추고 있습니다.

  4. 灵 活 性: FARM 스택은 간단한 CRUD 앱에서부터 복잡하고 데이터 집약적인 시스템까지 다양한 형태의 웹 애플리케이션을 적용할 수 있는 높은 유연성을 가지고 있습니다.

이러한 기술을 결합하면 FARM 스택은 현대적인 웹 애플리케이션을 구축하기 위한 종합적인 솔루션을 제공합니다. FastAPI로 빠르고 확장 가능한 백엔드, React로 直観적이고 반응형 프론트엔드, MongoDB로 유연하고 효율적인 데이터 저장을 가능하게 해줍니다. 이 스택은 실시간 업데이트, 복잡한 데이터 모델, 고성능이 요구되는 애플리케이션에 특히 적합합니다.

프로젝트 개요: Todo 애플리케이션

비디오 코스에서는 FARM Stack의 각 개별 기술에 대해 더 많이 다루고 있지만, 이 글에서는 모든 것을 합칠 프로젝트로 바로 들어가봅니다.

Todo 애플리케이션을 만들어 FARM 스택을 이해하는 데 도움을 줄 것입니다. 애플리케이션을 만들기 전에, 기능 및 소프트웨어 아키텍처에 대해 더 많이 다루어 봅니다.

Todo 애플리케이션의 기능

우리의 FARM 스택 todo 애플리케이션은 다음과 같은 기능을 포함할 것입니다:

  1. 다중 Todo 리스트:

    • 사용자는 다중 todo 리스트를 생성, 조회, 수정, 삭제할 수 있습니다.

    • 각 리스트는 이름을 가지고 여러 todo 항목을 포함합니다.

  2. Todo Items:

    • 在每个 list 내에서, 사용자는 todolist 아이템을 추가, 보기, 更新, 삭제할 수 있습니다.

    • 每个 아이템은 라벨이 있고, 체크/미체크 状态가 있으며, 特定的 list에 속합니다.

  3. Real-time Updates:

    • UI는 list 또는 아이템에 대한 변경 시에 실시간으로 更新되ます.
  4. Responsive Design:

    • 应用程序은 대상과 모바일 기기에서 alike로 响应用户动作하며 잘 동작합니다.

System architecture

Our todo application will follow a typical FARM stack architecture:

  1. 프론트엔드 (React):

    • 할 일 목록 및 항목과 상호 작용하는 사용자 인터페이스를 제공합니다.

    • RESTful API 호출을 통해 백엔드와 통신합니다.

  2. 백엔드 (FastAPI):

    • 프론트엔드로부터의 API 요청을 처리합니다.

    • 할 일 목록과 항목을 관리하는 비즈니스 로직을 구현합니다.

    • 데이터 지속성을 위해 MongoDB 데이터베이스와 상호 작용합니다.

  3. 데이터베이스 (MongoDB):

    • 할 일 데이터를 저장합니다.

    • 할 일 데이터의 효율적인 조회 및 업데이트를 제공합니다.

  4. Docker:

    • 각 组建(前端, 後端, databases)을 Containerize하여 開発과 배포를 簡単하게 해줍니다.

데이터 모델 설계

우리의 MongoDB 데이터 모델은 两大구성 요소로 구성되어 있습니다:

  1. ToDo List:
   {
     "_id": ObjectId,
     "name": String,
     "items": [
       {
         "id": String,
         "label": String,
         "checked": Boolean
       }
     ]
   }
  1. List Summary (모든 ToDo List에서 표시하기 위한 요약):
   {
     "_id": ObjectId,
     "name": String,
     "item_count": Integer
   }

API 엔드 포인트 설계

우리의 FastAPI 백端이 RESTful 엔드 포인트를 다음과 같이 노출합니다:

  1. ToDo Lists:

    • GET /api/lists: 모든 ToDo List을 가져올 수 있습니다 (요약 시각)

    • POST /api/lists: 새로운 ToDo List을 만들 수 있습니다

    • GET /api/lists/{list_id}: 특정 ToDo List의 모든 아이템을 가져올 수 있습니다

    • DELETE /api/lists/{list_id}: 특정 ToDo List을 지울 수 있습니다

  2. 할 일 항목:

    • POST /api/lists/{list_id}/items: 특정 목록에 새 항목을 추가하는 것

    • PATCH /api/lists/{list_id}/checked_state: 항목의 체크 상태를 更新하는 것

    • DELETE /api/lists/{list_id}/items/{item_id}: 목록에서 specific 항목을 삭제하는 것

이 프로젝트는 FARM 스택 開発과 Docker 컨테이너 사용에 대한 안정적인 기반을 제공할 것이며, 未来에 더 복잡한 응용 프로그램을 expand upon할 수 있는 것입니다.

그렇기 때문에 프로젝트를 시작해 보는 것을 시작해 보는 것입니다.

프로젝트 자습서

프로젝트 세팅과 백엔드 開発

단계 1: 프로젝트 구조를 설정하는 것

프로젝트에 대한 새로운 디렉터리를 생성하는 것:

   mkdir farm-stack-todo
   cd farm-stack-todo

백엔드와 프론트엔드를 위한 서브 디렉터리를 생성하는 것:

   mkdir backend frontend

단계 2: 백엔드 환경 설정하는 것

백엔드 디렉터리로 이동하는 것:

   cd backend

가상 환경을 생성하고 활성화하는 것:

   python -m venv venv
   source venv/bin/activate  # On Windows, use: venv\Scripts\activate

backend 디렉터리에以下 파일을 생성하세요.

    • Dockerfile

      • pyproject.toml

터미널에서 필요한 패키지를 설치하세요.

pip install "fastapi[all]" "motor[srv]" beanie aiostream

requirements.txt 파일을 생성하세요.

pip freeze > requirements.txt

requirements.txt 파일을 생성한 후 (pip-compile를 통해서든 직접든), 의존성을 다음과 같이 설치할 수 있습니다.

   pip install -r requirements.txt

Dockerfile에 다음 내용을 추가하세요.

   FROM python:3

   WORKDIR /usr/src/app
   COPY requirements.txt ./

   RUN pip install --no-cache-dir --upgrade -r ./requirements.txt

   EXPOSE 3001

   CMD [ "python", "./src/server.py" ]

pyproject.toml에 다음 내용을 추가하세요.

   [tool.pytest.ini_options]
   pythonpath = "src"

Step 4: 백엔드 구조 설정

backend 디렉터리 내에 src 디렉터리를 생성하세요.

   mkdir src

src 디렉터리 내에 다음 파일을 생성하세요.

Step 5: 데이터 액세스 레이어 (DAL) 구현

src/dal.py 파일을 열고 다음 내용을 추가하세요:

from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument

from pydantic import BaseModel

from uuid import uuid4

class ListSummary(BaseModel):
  id: str
  name: str
  item_count: int

  @staticmethod
  def from_doc(doc) -> "ListSummary":
      return ListSummary(
          id=str(doc["_id"]),
          name=doc["name"],
          item_count=doc["item_count"],
      )

class ToDoListItem(BaseModel):
  id: str
  label: str
  checked: bool

  @staticmethod
  def from_doc(item) -> "ToDoListItem":
      return ToDoListItem(
          id=item["id"],
          label=item["label"],
          checked=item["checked"],
      )

class ToDoList(BaseModel):
  id: str
  name: str
  items: list[ToDoListItem]

  @staticmethod
  def from_doc(doc) -> "ToDoList":
      return ToDoList(
          id=str(doc["_id"]),
          name=doc["name"],
          items=[ToDoListItem.from_doc(item) for item in doc["items"]],
      )

class ToDoDAL:
  def __init__(self, todo_collection: AsyncIOMotorCollection):
      self._todo_collection = todo_collection

  async def list_todo_lists(self, session=None):
      async for doc in self._todo_collection.find(
          {},
          projection={
              "name": 1,
              "item_count": {"$size": "$items"},
          },
          sort={"name": 1},
          session=session,
      ):
          yield ListSummary.from_doc(doc)

  async def create_todo_list(self, name: str, session=None) -> str:
      response = await self._todo_collection.insert_one(
          {"name": name, "items": []},
          session=session,
      )
      return str(response.inserted_id)

  async def get_todo_list(self, id: str | ObjectId, session=None) -> ToDoList:
      doc = await self._todo_collection.find_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return ToDoList.from_doc(doc)

  async def delete_todo_list(self, id: str | ObjectId, session=None) -> bool:
      response = await self._todo_collection.delete_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return response.deleted_count == 1

  async def create_item(
      self,
      id: str | ObjectId,
      label: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(id)},
          {
              "$push": {
                  "items": {
                      "id": uuid4().hex,
                      "label": label,
                      "checked": False,
                  }
              }
          },
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  async def set_checked_state(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      checked_state: bool,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id), "items.id": item_id},
          {"$set": {"items.$.checked": checked_state}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  async def delete_item(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id)},
          {"$pull": {"items": {"id": item_id}}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

이 튜토리얼의 부록 1을 마칩니다. 여기서 우리는 프로젝트 구조를 설정하고 FARM 스택 todo 애플리케이션의 데이터 액세스 레이어를 구현했습니다. 다음 부록에서는 FastAPI 서버를 구현하고 API 엔드포인트를 만듭니다.

FastAPI 서버 구현

스텝 6: FastAPI 서버 구현

src/server.py 파일을 열고 다음 내용을 추가하세요:

from contextlib import asynccontextmanager
from datetime import datetime
import os
import sys

from bson import ObjectId
from fastapi import FastAPI, status
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel
import uvicorn

from dal import ToDoDAL, ListSummary, ToDoList

COLLECTION_NAME = "todo_lists"
MONGODB_URI = os.environ["MONGODB_URI"]
DEBUG = os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes"}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 시작:
    client = AsyncIOMotorClient(MONGODB_URI)
    database = client.get_default_database()

    # 데이터베이스가 사용 가능한지 확인:
    pong = await database.command("ping")
    if int(pong["ok"]) != 1:
        raise Exception("Cluster connection is not okay!")

    todo_lists = database.get_collection(COLLECTION_NAME)
    app.todo_dal = ToDoDAL(todo_lists)

    # FastAPI 애플리케이션으로 돌아가기:
    yield

    # 종료:
    client.close()


app = FastAPI(lifespan=lifespan, debug=DEBUG)


@app.get("/api/lists")
async def get_all_lists() -> list[ListSummary]:
    return [i async for i in app.todo_dal.list_todo_lists()]


class NewList(BaseModel):
    name: str


class NewListResponse(BaseModel):
    id: str
    name: str


@app.post("/api/lists", status_code=status.HTTP_201_CREATED)
async def create_todo_list(new_list: NewList) -> NewListResponse:
    return NewListResponse(
        id=await app.todo_dal.create_todo_list(new_list.name),
        name=new_list.name,
    )


@app.get("/api/lists/{list_id}")
async def get_list(list_id: str) -> ToDoList:
    """Get a single to-do list"""
    return await app.todo_dal.get_todo_list(list_id)


@app.delete("/api/lists/{list_id}")
async def delete_list(list_id: str) -> bool:
    return await app.todo_dal.delete_todo_list(list_id)


class NewItem(BaseModel):
    label: str


class NewItemResponse(BaseModel):
    id: str
    label: str


@app.post(
    "/api/lists/{list_id}/items/",
    status_code=status.HTTP_201_CREATED,
)
async def create_item(list_id: str, new_item: NewItem) -> ToDoList:
    return await app.todo_dal.create_item(list_id, new_item.label)


@app.delete("/api/lists/{list_id}/items/{item_id}")
async def delete_item(list_id: str, item_id: str) -> ToDoList:
    return await app.todo_dal.delete_item(list_id, item_id)


class ToDoItemUpdate(BaseModel):
    item_id: str
    checked_state: bool


@app.patch("/api/lists/{list_id}/checked_state")
async def set_checked_state(list_id: str, update: ToDoItemUpdate) -> ToDoList:
    return await app.todo_dal.set_checked_state(
        list_id, update.item_id, update.checked_state
    )


class DummyResponse(BaseModel):
    id: str
    when: datetime


@app.get("/api/dummy")
async def get_dummy() -> DummyResponse:
    return DummyResponse(
        id=str(ObjectId()),
        when=datetime.now(),
    )


def main(argv=sys.argv[1:]):
    try:
        uvicorn.run("server:app", host="0.0.0.0", port=3001, reload=DEBUG)
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()

이 구현은 CORS 미들웨어를 사용하여 FastAPI 서버를 설정하고 MongoDB에 연결하며 todo 애플리케이션의 API 엔드포인트를 정의합니다.

스텝 7: 환경 변수 설정

루트 디렉토리에 .env 파일을 생성하고 다음 내용을 추가하세요. “.mongodb.net/” 끝에 데이터베이스 이름(“todo”)을 추가하십시오.

MONGODB_URI='mongodb+srv://beau:codecamp@cluster0.ji7hu.mongodb.net/todo?retryWrites=true&w=majority&appName=Cluster0'

스텝 8: docker-compose 파일 생성

프로젝트의 루트 디렉토리(farm-stack-todo)에 compose.yml 파일을 생성하고 다음 내용을 추가하세요:

name: todo-app
services:
  nginx:
    image: nginx:1.17
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 8000:80
    depends_on:
      - backend
      - frontend
  frontend:
    image: "node:22"
    user: "node"
    working_dir: /home/node/app
    environment:
      - NODE_ENV=development
      - WDS_SOCKET_PORT=0
    volumes:
      - ./frontend/:/home/node/app
    expose:
      - "3000"
    ports:
      - "3000:3000"
    command: "npm start"
  backend:
    image: todo-app/backend
    build: ./backend
    volumes:
      - ./backend/:/usr/src/app
    expose:
      - "3001"
    ports:
      - "8001:3001"
    command: "python src/server.py"
    environment:
      - DEBUG=true
    env_file:
      - path: ./.env
        required: true

스텝 9: Nginx 구성 설정

프로젝트의 루트에 nginx라는 디렉토리를 생성하세요:

mkdir nginx

nginx 디렉터리 안에 nginx.conf 파일을 생성하고 다음 내용을 넣으세요:

server {
    listen 80;
    server_name farm_intro;

    location / {
        proxy_pass http://frontend:3000;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /api {
        proxy_pass http://backend:3001/api;
    }
}

이 튜토리얼의 2부분을 마칩니다. 여기서 우리는 FastAPI 서버를 구현하고, 환경 변수를 설정하고, docker-compose 파일을 만들고, Nginx를 구성했습니다. 다음 부분에서는 FARM 스택 todo 애플리케이션의 React 프론트엔드를 설정하는 것에 초점을 맞출 것입니다.

React 프론트엔드 설정

10단계: React 애플리케이션 만들기

frontend 디렉터리로 이동하세요:

cd ../frontend

Create React App을 사용하여 새 React 애플리케이션을 만듭니다:

npx create-react-app .

추가적인 의존성을 설치합니다:

   npm install axios react-icons

11단계: 주요 App 컴포넌트 설정

src/App.js의 내용을 다음과 같이 바꿉니다:

import { useEffect, useState } from "react";
import axios from "axios";
import "./App.css";
import ListToDoLists from "./ListTodoLists";
import ToDoList from "./ToDoList";

function App() {
  const [listSummaries, setListSummaries] = useState(null);
  const [selectedItem, setSelectedItem] = useState(null);

  useEffect(() => {
    reloadData().catch(console.error);
  }, []);

  async function reloadData() {
    const response = await axios.get("/api/lists");
    const data = await response.data;
    setListSummaries(data);
  }

  function handleNewToDoList(newName) {
    const updateData = async () => {
      const newListData = {
        name: newName,
      };

      await axios.post(`/api/lists`, newListData);
      reloadData().catch(console.error);
    };
    updateData();
  }

  function handleDeleteToDoList(id) {
    const updateData = async () => {
      await axios.delete(`/api/lists/${id}`);
      reloadData().catch(console.error);
    };
    updateData();
  }

  function handleSelectList(id) {
    console.log("Selecting item", id);
    setSelectedItem(id);
  }

  function backToList() {
    setSelectedItem(null);
    reloadData().catch(console.error);
  }

  if (selectedItem === null) {
    return (
      <div className="App">
        <ListToDoLists
          listSummaries={listSummaries}
          handleSelectList={handleSelectList}
          handleNewToDoList={handleNewToDoList}
          handleDeleteToDoList={handleDeleteToDoList}
        />
      </div>
    );
  } else {
    return (
      <div className="App">
        <ToDoList listId={selectedItem} handleBackButton={backToList} />
      </div>
    );
  }
}

export default App;

12단계: ListTodoLists 컴포넌트 만들기

src/ListTodoLists.js라는 새 파일을 다음 내용으로 만듭니다:

import "./ListTodoLists.css";
import { useRef } from "react";
import { BiSolidTrash } from "react-icons/bi";

function ListToDoLists({
  listSummaries,
  handleSelectList,
  handleNewToDoList,
  handleDeleteToDoList,
}) {
  const labelRef = useRef();

  if (listSummaries === null) {
    return <div className="ListToDoLists loading">Loading to-do lists ...</div>;
  } else if (listSummaries.length === 0) {
    return (
      <div className="ListToDoLists">
        <div className="box">
        <label>
          New To-Do List:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
        </div>
        <p>There are no to-do lists!</p>
      </div>
    );
  }
  return (
    <div className="ListToDoLists">
      <h1>All To-Do Lists</h1>
      <div className="box">
        <label>
          New To-Do List:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
      </div>
      {listSummaries.map((summary) => {
        return (
          <div
            key={summary.id}
            className="summary"
            onClick={() => handleSelectList(summary.id)}
          >
            <span className="name">{summary.name} </span>
            <span className="count">({summary.item_count} items)</span>
            <span className="flex"></span>
            <span
              className="trash"
              onClick={(evt) => {
                evt.stopPropagation();
                handleDeleteToDoList(summary.id);
              }}
            >
              <BiSolidTrash />
            </span>
          </div>
        );
      })}
    </div>
  );
}

export default ListToDoLists;

src/ListTodoLists.css라는 새 파일을 다음 내용으로 만듭니다:

.ListToDoLists .summary {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
    cursor: pointer;
    display: flex;
}

.ListToDoLists .count {
    padding-left: 1ex;
    color: blueviolet;
    font-size: 92%;
}

13단계: ToDoList 컴포넌트 만들기

src/ToDoList.js라는 새 파일을 다음 내용으로 만듭니다:

import "./ToDoList.css";
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import { BiSolidTrash } from "react-icons/bi";

function ToDoList({ listId, handleBackButton }) {
  let labelRef = useRef();
  const [listData, setListData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get(`/api/lists/${listId}`);
      const newData = await response.data;
      setListData(newData);
    };
    fetchData();
  }, [listId]);

  function handleCreateItem(label) {
    const updateData = async () => {
      const response = await axios.post(`/api/lists/${listData.id}/items/`, {
        label: label,
      });
      setListData(await response.data);
    };
    updateData();
  }

  function handleDeleteItem(id) {
    const updateData = async () => {
      const response = await axios.delete(
        `/api/lists/${listData.id}/items/${id}`
      );
      setListData(await response.data);
    };
    updateData();
  }

  function handleCheckToggle(itemId, newState) {
    const updateData = async () => {
      const response = await axios.patch(
        `/api/lists/${listData.id}/checked_state`,
        {
          item_id: itemId,
          checked_state: newState,
        }
      );
      setListData(await response.data);
    };
    updateData();
  }

  if (listData === null) {
    return (
      <div className="ToDoList loading">
        <button className="back" onClick={handleBackButton}>
          Back
        </button>
        Loading to-do list ...
      </div>
    );
  }
  return (
    <div className="ToDoList">
      <button className="back" onClick={handleBackButton}>
        Back
      </button>
      <h1>List: {listData.name}</h1>
      <div className="box">
        <label>
          New Item:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleCreateItem(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
      </div>
      {listData.items.length > 0 ? (
        listData.items.map((item) => {
          return (
            <div
              key={item.id}
              className={item.checked ? "item checked" : "item"}
              onClick={() => handleCheckToggle(item.id, !item.checked)}
            >
              <span>{item.checked ? "✅" : "⬜️"} </span>
              <span className="label">{item.label} </span>
              <span className="flex"></span>
              <span
                className="trash"
                onClick={(evt) => {
                  evt.stopPropagation();
                  handleDeleteItem(item.id);
                }}
              >
                <BiSolidTrash />
              </span>
            </div>
          );
        })
      ) : (
        <div className="box">There are currently no items.</div>
      )}
    </div>
  );
}

export default ToDoList;

src/ToDoList.css라는 새 파일을 다음 내용으로 만듭니다:

.ToDoList .back {
    margin: 0 1em;
    padding: 1em;
    float: left;
}

.ToDoList .item {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
    cursor: pointer;
    display: flex;
}

.ToDoList .label {
    margin-left: 1ex;
}

.ToDoList .checked .label {
    text-decoration: line-through;
    color: lightgray;
}

14단계: 주요 CSS 파일 업데이트

src/index.css의 내용을 다음과 같이 바꿉니다:

html, body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-size: 12pt;
}

input, button {
  font-size: 1em;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.box {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
}

.flex {
  flex: 1;
}

이 튜토리얼의 3부분을 마칩니다. 여기서 우리는 FARM 스택 todo 애플리케이션의 React 프론트엔드를 설정했습니다. 주요 App 컴포넌트, 모든 todo 리스트를 표시하는 ListTodoLists 컴포넌트, 그리고 개별 todo 리스트를 위한 ToDoList 컴포넌트를 만들었습니다. 다음 부분에서는 애플리케이션을 실행하고 테스트할 것입니다.

稼働 및 응용 테스트

스텝 18: Docker Compose를 사용하여 응용을 실행하기

  1. Docker과 Docker Compose를 시스템에 이미 설치한 것을 확인하세요

  2. 프로젝트(farm-stack-todo)의 루트 디렉터리에 终端을 열세요

  3. 컨테이너를 빌드하고 시작하세요:

docker-compose up --build
  1. 컨테이너가 시작되고 동작하는 것을 확인한 다음, 웹 브라우저를 열고 http://localhost:8000에 가세요

스텝 19: 응용을 중지하기

  1. Docker를 사용하지 않고 응용을 실행하는 경우:

    • React 開発 서버를 Ctrl+C 키를 눌러 终端에서 중지하세요

    • FastAPI 서버를 Ctrl+C 키를 눌러 终端에서 중지하세요

    • MongoDB 서버를 Ctrl+C 키를 눌러 终端에서 중지하세요

  2. Docker Compose로 애플리케이션을 실행 중이라면:

    • docker-compose up 명령을 실행한 터미널에서 Ctrl+C를 누르세요

    • 컨테이너를 중지하고 제거하려면 다음 명령을 실행하세요:

     docker-compose down

“`

축하합니다! FARM 스택 todo 애플리케이션을 성공적으로 빌드하고 테스트했습니다. 이 애플리케이션은 FastAPI, React, 그리고 MongoDB가 풀 스택 웹 애플리케이션에서 통합되는 것을 보여줍니다.

애플리케이션을 개선하기 위한 다음 단계를 고려하세요:

  1. 사용자 인증 및 권한 부여 추가

  2. 데이터 유효성 검사 및 에러 처리 구현

  3. todo 항목의 마감일, 우선순위, 또는 카테고리와 같은 추가 기능 추가

  4. 더욱 깔끔한 디자인으로 UI/UX를 개선

  5. 앞단과 뒷단 모두의 단위 및 통합 테스트를 작성하십시오

  6. 애플리케이션의 지속적인 통합 및 배포 (CI/CD)를 설정하십시오

애플리케이션을 개발하면서 의존성을 최신 상태로 유지하고 보안과 성능에 관한 最佳實踐을 따르도록 하십시오.

결론과 다음 단계

이 포괄적인 FARM 스택 자습서를 완료하신 것을 축하드립니다! todo 애플리케이션을 빌드하면서 현대 웹 개발에서 가장 강력하고 popularr한 기술들을 경험해보았습니다. FastAPI로 강健壮한 백엔드 API를 만들고, React로 동적이고 반응적인 앞단을 구축하며, MongoDB로 데이터를 영속화하고 Docker를 사용하여 전체 애플리케이션을 컨테이너화했습니다. 이 프로젝트는 이러한 기술이 어떻게 호환되어 완전한 기능을 갖춘 규모가능한 웹 애플리케이션을 만드는지 보여주었습니다.