<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>earthquake_woo.log</title>
        <link>https://velog.io/</link>
        <description>높이보다는 멀리, 넓게보다는 깊게</description>
        <lastBuildDate>Sun, 17 Dec 2023 14:38:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>earthquake_woo.log</title>
            <url>https://velog.velcdn.com/images/earthquake_woo/profile/7878f5f8-4c9e-4f5e-982a-50a8affa3bfd/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. earthquake_woo.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/earthquake_woo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[FastAPI 프로젝트 배포:CI/CD]]></title>
            <link>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACCICD</link>
            <guid>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACCICD</guid>
            <pubDate>Sun, 17 Dec 2023 14:38:42 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p>이 시리즈를 통해 FastAPI 프로젝트를 배포했습니다. 하지만 서비스가 업데이트될 때마다 이미지를 ECR에 업로드하고 ECS 작업 정의를 생성한 뒤 업데이트를 해야하는 과정은 꽤나 번거롭습니다.</p>
<p>이번 포스팅에선 이 번거로운 작업들을 자동화하기 위해 CI/CD를 구축하는 방법을 살펴보겠습니다.</p>
<h1 id="2-github-repository-생성">2. Github Repository 생성</h1>
<p>먼저 github에세 새로운 레포지토리를 생성합니다. 이후 &quot;Setting&quot;에 들어와서 왼쪽 &quot;Security&quot; 패널의 &quot;Secrets and variables&quot;를 클릭하여 &quot;New repository secret&quot;을 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/9a4258fc-3cf5-47bb-8ddc-1d65aac54fdd/image.PNG" alt=""></p>
<p>여기서 AWS IAM 사용자 생성을 했을 때 발급 받은 액세스 키로 아래와 같이 <code>Name</code>과 <code>Secret</code>을 설정하고 환경변수를 각각 생성합니다.</p>
<ul>
<li><code>AWS_ACCESS_KEY_ID</code> : 발급받은 aws access key</li>
<li><code>AWS_SECRET_ACCESS_KEY</code> : 발급받은 aws secret access key</li>
</ul>
<p>환경변수가 생성되었다면 &quot;Actions&quot; 탭에서 <code>Deploy to Amazon ECs</code>를 검색해 선택합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/f12cca9b-9226-4cda-83f0-1aa7e10a283a/image.PNG" alt=""></p>
<p>이제 <code>aws.yml</code>의 <code>env</code>에서 아래의 항목을 수정해야합니다.</p>
<ul>
<li><code>AWS_REGION</code>: your aws region</li>
<li><code>ECR_REPOSITORY</code>: AWS ECR Repository name</li>
<li><code>ECS_SERVICE</code>: AWS ECS Service name</li>
<li><code>ECS_CLUSTER</code>: AWS ECS Cluster name</li>
<li><code>ECS_TASK_DEFINITION</code>: <code>.aws/task-definition.json</code> </li>
</ul>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/78fa9966-d367-4969-8001-7bde98f90241/image.PNG" alt=""></p>
<p>설정이 되었다면 프로젝트 루트 디렉토리에 <code>.aws</code> 폴더를 생성하고 그 안에 <code>task-definition.json</code> 파일을 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/862e9efa-7a9f-4a66-843a-2e6ee1246d08/image.PNG" alt=""></p>
<p>이후 <a href="https://us-east-1.console.aws.amazon.com/ecs/v2/clusters?region=us-east-1">AWS ECS</a>에서 테스크 정의에 생성된 테스크 정의의 JSON 내용을 복사해 <code>task-definition.json</code>에 붙여넣기 합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/a9c6ac1e-5fdd-485f-933f-e06578b217ad/image.PNG" alt=""></p>
<p>이제 다시 github &quot;Actions&quot;으로 돌아와 &quot;Commit&quot;을 눌러줍니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/1ddd43b4-2aa3-48ad-92db-ad310f09134b/image.PNG" alt=""></p>
<h1 id="3-cicd">3. CI/CD</h1>
<p>이 <code>aws.yml</code> 파일은 새 버전의 서비스를 AWS ECS에 배포하기 위해 수행해야 하는 작업을 정의합니다. 사전에 정의된 동작은 main branch로 push할 때마다 github action이 배포를 시작하는 것입니다.</p>
<p>이제 git을 초기화하고 프로젝트 디렉토리와 연결해 push 작업을 진행해야합니다. <code>git clone</code>으로 해당 레포지토리를 가져온 후 프로젝트를 해당 레포지토리에 복사한 후 push합니다.</p>
<pre><code class="language-bash">git clone &lt;your repository url&gt; &lt;dir name&gt;

git add .
git commit -m &quot;first commit&quot;
git push -u origin main</code></pre>
<p>그리고 &quot;Actions&quot; 탭으로 이동하면 워크플로우의 단계별 절차를 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/db8e1ee5-16e2-4f8c-b806-d46bdc3ce64a/image.PNG" alt=""></p>
<p>잠시 후 배포가 완료된 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/c3138e31-8903-441f-b528-81292a254ebd/image.PNG" alt=""></p>
<h1 id="4-테스트">4. 테스트</h1>
<p>이제 코드를 수정해 github action을 통해서 재배포를 해보겠습니다.</p>
<pre><code class="language-python">import os
from fastapi import FastAPI

app = FastAPI()

@app.get(&#39;/&#39;)
def welcome_root():
    my_name = os.environ.get(&quot;MY_NAME&quot;)
    return f&quot;Hello {my_name}!&quot;</code></pre>
<p>변경사항을 수정 후 main 브랜치에 푸시합니다.</p>
<pre><code class="language-bash">git add .
git commit -m &quot;update code&quot;
git push</code></pre>
<p>Github &quot;Actions&quot; 탭으로 이동해 새배포가 완료되었으면 브라우저를 열어 확인합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/92f41ae7-e98e-4ff1-a3b2-eeaf709896c8/image.PNG" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 프로젝트 배포:ECS Fargate 배포]]></title>
            <link>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACECS-Fargate-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACECS-Fargate-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Sat, 16 Dec 2023 12:19:39 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p>이전 포스팅에선 FastAPI 서버의 컨테이너 이미지를 AWS ECR에 업로드했습니다.</p>
<p>이제 서버를 ECS Fargate에 배포해보도록 하겠습니다.</p>
<h1 id="2-albapplication-load-balancer-생성">2. ALB(Application Load Balancer) 생성</h1>
<p><a href="https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#Home:">AWS EC2</a>로 이동해 &quot;로드 밸런싱&quot;의 &quot;로드밸러서&quot;로 이동해 로드밸러스를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/90342d56-d986-4268-9955-ffc199cb0350/image.PNG" alt=""></p>
<p>로드 밸런서 유형은 &quot;Application Load Balancer&quot;로 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/df1c6900-e7a3-4740-bc6f-9e081004f01a/image.PNG" alt=""></p>
<p>로드 밸런서의 이름을 설정하고 네트워크 매핑에서 VPC를 선택합니다. 만약 VPC가 없다면 <a href="https://us-east-1.console.aws.amazon.com/vpcconsole/home?region=us-east-1#vpcs:">VPC</a>로 이동해서 VPC를 생성해줍니다. </p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/9c0dae36-8fb9-4629-902b-b72ab9e4248b/image.PNG" alt=""></p>
<p>최소 2개의 가용영역과 영역당 하나의 서브넷이 필요합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/f61d33e0-29d5-4526-9883-30cf2c74c378/image.PNG" alt=""></p>
<p>새로운 창을 열어 <a href="https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#SecurityGroups:">AWS EC2 보안그룹</a>에서 HTTP 요청에 80 포트를 허용하는 보안그룹을 생성합니다. 생성한 VPC를 선택하고 인바운드 규칙에서 아래와 같이 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/bc6275b4-9d64-4192-a93d-dad764508a10/image.PNG" alt=""></p>
<p>보안그룹에서 새로고침을 눌러 생성한 보안그룹을 적용합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/245daf7f-412b-488f-b414-13c446fd5e9d/image.PNG" alt=""></p>
<p>이제 타켓이 되는 그룹을 선택해야합니다. <a href="https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#TargetGroups:">AWS EC2 대상 그룹</a> 으로 이동해 대상 그룹 생성을 합니다. </p>
<p>로드 밸런싱을 지원하는 &quot;IP 주소&quot;를 선택하고 생성한 VPC를 선택 후 다음과 같이 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/bdd1e0ef-d9be-4c01-b480-82f9bdbaf6df/image.PNG" alt=""></p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/47c00e40-f499-4127-9a29-3dee1b690105/image.PNG" alt=""></p>
<p>다시 로드 밸런서로 돌아와 리스너 및 라우팅에서 새로고침을 누르고 생성한 대상 그룹을 선택 후 로드 밸런서를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/8242fb97-3a05-40aa-be14-608c5edaacb9/image.PNG" alt=""></p>
<h1 id="3-ecselastic-container-service">3. ECS(Elastic Container Service)</h1>
<p>이제 <a href="https://us-east-1.console.aws.amazon.com/ecs/v2/clusters?region=us-east-1">AWS ECS</a> 로 이동해 클러스터를 생성합니다. AWS Fargate로 배포할 예정이니 인프라를 AWS Fargate로 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/2c5571c3-82f5-4ba7-97e8-bb8558358978/image.PNG" alt=""></p>
<p>AWS ECS의 테스크 정의로 이동해 새 테스크 정의 생성을 합니다. 테스크 정의는 포트 매핑, 도커 이미지, CPU, 메모리 사용량 등과 같은 서비스의 동작 및 설정을 정의하는 <code>docker-compose</code>과 유사합니다.</p>
<p>AWS Fargate를 선택하고 각자에 필요에 따라 CPU와 메모리 사용량을 설정하고 테스크 역할을 &quot;ecsTaskExecutionRole&quot;로 선택합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/30e5deac-f295-4c75-a637-9b06665bfa72/image.PNG" alt=""></p>
<p>이제 컨테이너 포트를 매핑해야합니다. 이전에 생성했던 컨테이너 이미지의 이름과 URL을 ECR Repogitories에서 가져옵니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/78c510ba-b7a7-4ce0-906b-ef667e6491df/image.PNG" alt=""></p>
<p>아래와 같이 이미지의 이름과 URL을 설정합니다. 또한 컨테이너는 8000포트를 수신하기 때문에 컨테이너 포트가 8000임을 나타내는 포트 매핑을 정의합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/4610330b-3e69-4d30-aed2-072ee737ba07/image.PNG" alt=""></p>
<p>이후 테스크를 생성하고 다시 클러스터로 이동해 생성한 클러스터에서 서비스를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/0065a0b2-4657-4408-a57b-da63f80bd715/image.PNG" alt=""></p>
<p>컴퓨팅 옵션을 시작 유형으로 선택합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/ef872570-57cc-45a3-86b7-29583f752e99/image.PNG" alt=""></p>
<p>패밀리를 이전에 생성한 테스크 정의를 선택하고 아래와 같이 배포구성을 합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/4b35830f-76a5-4fce-b5da-9de466aaa452/image.PNG" alt=""></p>
<p>아래로 이동해 네트워킹 설정을 합니다. 생성한 VPC와 서브넷을 선택한 후 아래와 같이 보안그룹을 생성합니다. 이전에 컨테이너를 8000 포트에 매핑했으므로 사용자 지정으로 TCP 8000 포트를 허용하는 보안 그룹을 생성해야합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/2b06b63f-a3f4-44b2-a070-a9e598b60ad7/image.PNG" alt=""></p>
<p>이제 로드 밸런싱으로 이동해 생성한 로드 밸랜싱을 연결합니다. 기존 로드 밸런서에서 생성한 로드 밸런서를 선택한 후 생성한 대상 그룹을 적용합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/ff7a3277-5fa8-4cd2-8850-de09f793b703/image.PNG" alt=""></p>
<p>서비스 성공적으로 배포되면 아래와 같이 활성화 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/da3416c4-5f59-4515-b2ce-d02929aea596/image.PNG" alt=""></p>
<p>이제 테스트를 위해 EC2의 로드밸런서로 이동해 DNS 이름으로 브라우저에서 접근합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/1e2338e7-45a4-41e7-be2b-86011f67f69f/image.PNG" alt=""></p>
<p>짠! 제대로 동작하는 것을 확인할 수 있습니다.</p>
<p><img src="blob:https://velog.io/7178e7e9-48d9-4dca-a12a-609f8ec8c4cd" alt="업로드중.."></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python exceptions with FastAPI Handling Errors]]></title>
            <link>https://velog.io/@earthquake_woo/Python-exceptions-with-FastAPI-Handling-Errors</link>
            <guid>https://velog.io/@earthquake_woo/Python-exceptions-with-FastAPI-Handling-Errors</guid>
            <pubDate>Sat, 16 Dec 2023 05:10:18 GMT</pubDate>
            <description><![CDATA[<h1 id="introduction">Introduction</h1>
<p>Pyhton에서 코드를 구현하다보면 여러 에러를 맞이하게 됩니다. 이렇듯 코드를 실행하는 중에 발생한 에러는 exception(예외)라 합니다.</p>
<p>예외처리를 하기위해 python에서는 <code>try</code> <code>except</code> 구문이 존재합니다. <code>try</code>에 실행할 코드를 구현하고 <code>except</code>에 예외가 발생했을 때 처리하는 코드를 구현합니다.</p>
<p>이 포스팅에선 <code>try</code> <code>except</code>에 대해 살펴보고 사용자 정의 예외를 구현하고 이를 FastAPI에서 적용시켜보고 저의 <a href="https://github.com/earthquakoo/jobdam">Jobdam</a>에 어떻게 적용되었는지 확인해보겠습니다!</p>
<h1 id="handling-exceptions">Handling Exceptions</h1>
<p>에러는 문장이나 표현식이 올바르다 할지라도 실행할 때 에어를 일으킬 수 있습니다. 실행 중에 감지되는 에러들을 예외(exception)라 부르고 이는 무조건 치명적이지는 않습니다. 아래는 올바른 표현식이나 프로그램이 처리하지 않은 에러의 예시입니다.</p>
<pre><code class="language-python">print(10 * (1/0))</code></pre>
<pre><code class="language-bash">Traceback (most recent call last):
  File &quot;C:\Users\cream\desktop\reference\main.py&quot;, line 1, in &lt;module&gt;
    print(10 * (1/0))
                ~^~
ZeroDivisionError: division by zero</code></pre>
<p>에러의 마지막줄의 <code>ZeroDivisionError</code> 는 내장 예외 중 하나입니다. 에러의 나머지는 예외의 형태와 원인에 기반을 둔 상세 내용을 제공합니다.</p>
<p>이러한 예외들을 <code>try</code> <code>except</code> 구문을 통해 선택적으로 처리할 수 있습니다. 아래는 올바른 정수가 입력될 때까지 예외를 일으키는 예시입니다.</p>
<pre><code class="language-python">while True:
    try:
        x = int(input(&quot;Please enter a number: &quot;))
        break
    except ValueError:
        print(&quot;Oops!  That was no valid number.  Try again...&quot;)</code></pre>
<ul>
<li>먼저 <code>try</code> 바로 아래의 코드가 실행됩니다.</li>
<li>예외가 발생하지 않으면 <code>except</code>를 건너뛰고 <code>try</code> 문의 실행은 종료됩니다.</li>
<li>예외가 발생하면 <code>try</code> 구문의 나머지를 건너뛰고 <code>except</code> 에서 해당 유형의 에러가 발생하면 예외처리가 됩니다.</li>
<li>만약 <code>except</code>에서 제시된 유형의 에러가 발생하지않으면 에러 메시지와 함께 실행이 종료됩니다.</li>
</ul>
<p><code>BaseException</code>의 하위 클래스 중 하나인 <code>Exception</code>은 치명적이지 않은 모든 예외의 기본 클래스입니다. (<a href="https://docs.python.org/3/library/exceptions.html#exception-hierarchy">예외 계층</a> 은 여기서 확인 가능합니다.) 만약 위의 예시에서 무슨 에러가 발생하는지 잘 모르겠다면 <code>Exception</code>을 통해 거의 모든 예외를 처리할 수 있습니다. </p>
<pre><code class="language-python">while True:
    try:
        x = int(input(&quot;Please enter a number: &quot;))
        break
    except Exception:
        print(&quot;Oops!  That was no valid number.  Try again...&quot;)</code></pre>
<p>하지만 처리하려는 예외 유형을 최대한 구체적으로 지정하는 것이 좋습니다. 만약 더 정확한 에러를 알고 싶다면 <code>except</code> 구문에서 <code>as</code> 뒤에 변수를 지정해서 에러 메시지를 확인할 수 있습니다.</p>
<pre><code class="language-python">while True:
    try:
        x = int(input(&quot;Please enter a number: &quot;))
        break
    except Exception as e:
        print(&quot;Oops!  That was no valid number.  Try again...&quot;)
        print(e)</code></pre>
<pre><code class="language-bash">Please enter a number: s
Oops!  That was no valid number.  Try again...
invalid literal for int() with base 10: &#39;s&#39;</code></pre>
<h1 id="raising-exceptions">Raising Exceptions</h1>
<p><code>raise</code> 구문은 사용자가 직접 지정한 예외가 발생하도록 강제할 수 있습니다. <code>raise</code>는 예외 인스턴스거나 예외 클래스(<code>BaseException</code> 하위 클래스) 중 하나 이어야 합니다.</p>
<p>아래는 <code>raise</code> 구문으로 직접 예외를 발생하도록 처리하고 <code>try</code> <code>except</code> 에서 처리한 예외를 다시 발생시키는 예시입니다.</p>
<pre><code class="language-python">def test_exception():
    try:
        x = int(input(&quot;Please enter a number: &quot;))
        if x % 2 != 0:
            raise Exception(&#39;Oops!  That was no valid number&#39;)
        print(x)
    except Exception as e:
        print(&#39;Exception in function&#39;, e)
        raise

try:
    test_exception()
except Exception as e:
    print(&#39;Exception in parent code&#39;, e)</code></pre>
<pre><code class="language-bash">Please enter a number: ;
Exception in function invalid literal for int() with base 10: &#39;;&#39;
Exception in parent code invalid literal for int() with base 10: &#39;;&#39;</code></pre>
<h1 id="fastapi-handling-errors--custom-exception-handlers">FastAPI Handling Errors &amp; Custom exception handlers</h1>
<p>FastAPI에선 일반적으로 <code>HTTPException</code>을 이용해 <code>status_code</code>와 에러 메시지인 <code>detail</code>로 예외처리를 합니다.</p>
<pre><code class="language-python">from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {&quot;foo&quot;: &quot;The Foo Wrestlers&quot;}


@app.get(&quot;/items/{item_id}&quot;)
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail=&quot;Item not found&quot;)
    return {&quot;item&quot;: items[item_id]}</code></pre>
<p>클라이언트가 정상 요청을 할 경우 HTTP 상태코드인 200과 함께 다음 JSON 응답을 받게 됩니다.</p>
<pre><code class="language-bash">{
  &quot;item&quot;: &quot;The Foo Wrestlers&quot;
}</code></pre>
<p>비정상 요청인 경우 HTTP 상태코드인 404와 함께 다음 JSON 응답을 받게 됩니다.</p>
<pre><code class="language-bash">{
  &quot;detail&quot;: &quot;Item not found&quot;
}</code></pre>
<p>이제 간단한 사용자 정의 예외를 만들어 이 예외를 전역적으로 처리하는 코드를 작성해보겠습니다. <code>UnicornException</code> 이라는 사용자 정의 예외를 생성하고 <code>@app.exception_handler()</code>를 통해 사용자 정의 예외를 추가했습니다.</p>
<pre><code class="language-python">from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse


class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name


app = FastAPI()


@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={&quot;message&quot;: f&quot;Oops! {exc.name} did something. There goes a rainbow...&quot;},
    )


@app.get(&quot;/unicorns/{name}&quot;)
async def read_unicorn(name: str):
    if name == &quot;yolo&quot;:
        raise UnicornException(name=name)
    return {&quot;unicorn_name&quot;: name}</code></pre>
<p><code>/unicorns/yolo</code>로 요청하면 <code>raise UnicornException</code> 예외가 발생하고 이는 <code>unicorn_exception_handler</code> 에서 처리하고 다음과 같은 응답을 받게 됩니다.</p>
<pre><code class="language-bash">{&quot;message&quot;: &quot;Oops! yolo did something. There goes a rainbow...&quot;}</code></pre>
<h1 id="user-defined-exceptions-with-fastapi">User defined Exceptions with FastAPI</h1>
<p>이것만으로도 충분한 사용자 예외 처리를 할 수 있어보입니다. 하지만 프로젝트를 진행하다보면 수많은 예외를 발생시켜야 할 때가 많습니다. 그렇기에 메인이 되는 <code>BaseException</code> 부모 클래스를 생성하고 이를 상속받아 FastAPI에서 <code>exception_handler</code>로 정의하면 재활용이 쉽고, 손쉽게 예외 처리를 할 수 있습니다.</p>
<p>저는 FastAPI에서 회원가입이 되지 않은 회원이 로그인을 시도하려할 때 발생시킬 사용자 정의 예외를 만드려고 합니다.</p>
<p>아래와 같이 <code>Exception</code> 클래스를 상속받아 사용자 정의 예외를 만들고 python 내장 클래스인 <code>__str__</code>을 정의해 객체가 print 함수에 전달될 경우 객체 내부의 <code>detail</code>을 반환하도록 했습니다.</p>
<pre><code class="language-python">class BaseCustomException(Exception):
    &quot;&quot;&quot;Base class for custom exceptions&quot;&quot;&quot;

    def __init__(self, status_code: int, detail: str):
        self.status_code = status_code
        self.detail = detail

    def __str__(self):
        return self.detail</code></pre>
<p>직접 정의한 <code>BaseCustomException</code>을 상속받아 <code>UnregisteredUserError</code> 예외 클래스를 생성합니다. 여기선 <code>super().__init__</code> 을 통해 부모 클래스인 <code>BaseCutomException</code>의 인스턴스를 가져옵니다.</p>
<pre><code class="language-python">...

class UnregisteredUserError(BaseCustomException):
    def __init__(self, user_name: str):
        super().__init__(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f&quot;{user_name} is an unregistered user.&quot;
        )</code></pre>
<p>그리고 직접 정의한 <code>BaseCustomException</code>을 FastAPI에서 <code>exception_handler</code>로 사용하기 위한 handler를 생성합니다.</p>
<pre><code class="language-python">...

def base_custom_exception_handler(request: Request, exc: BaseCustomException):
    return JSONResponse(status_code=exc.status_code, content={&quot;detail&quot;: exc.detail})</code></pre>
<p>이를 <code>add_exception_handler</code> 를 통해 예외 핸들러를 추가해줍니다.</p>
<pre><code class="language-python">...
app = FastAPI()

...
app.add_exception_handler(BaseCustomException, base_custom_exception_handler)</code></pre>
<p>이것이 실제로 적용된 코드가 궁금하다면 <a href="https://github.com/earthquakoo/jobdamserver">Jobdamserver</a>에서 확인이 가능합니다!</p>
<blockquote>
<p>reference</p>
<ul>
<li><a href="https://docs.python.org/ko/3/tutorial/errors.html">Python 공식문서 exceptions</a></li>
<li><a href="https://fastapi.tiangolo.com/tutorial/handling-errors/">FastAPI 공식문서 handling error</a></li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI와 Requests 라이브러리로 HTTP 통신 구현]]></title>
            <link>https://velog.io/@earthquake_woo/FastAPI%EC%99%80-Requests-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-HTTP-%ED%86%B5%EC%8B%A0-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@earthquake_woo/FastAPI%EC%99%80-Requests-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-HTTP-%ED%86%B5%EC%8B%A0-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 16 Dec 2023 05:00:17 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p><a href="https://github.com/earthquakoo/jobdamserver">Jobdam</a> 프로젝트에서 Python을 사용하여 HTTP 통신을 구현해야 했습니다. </p>
<p>저는 Python 기반의 TUI(Terminal User Interface)인 <a href="https://textual.textualize.io/">Textual</a>로 터미널 앱 <a href="https://github.com/earthquakoo/jobdamserver">Jobdam</a>을 개발한 경험이 있습니다. 또한, 해당 프로젝트의 서버는 Python 기반의 FastAPI를 사용하여 구축되었기 때문에 Python을 이용한 HTTP 통신이 필수적이었습니다.</p>
<p>이를 위해 python으로 HTTP 통신을 가능하게 해주는 <a href="https://pypi.org/project/requests/">requests</a>라이브러리를 사용하게 되었습니다. 이번 포스팅에서는 requests 라이브러리에 대한 개요와 Jobdam 프로젝트에서의 적용 방법에 대해 알아보겠습니다.</p>
<h1 id="2-환경설정-및-requests-라이브러리-개요">2. 환경설정 및 Requests 라이브러리 개요</h1>
<p>Requests 라이브러리는 HTTP 통신을 위한 다양한 메서드를 제공합니다. 이를 이용하여 REST API 방식의 Web API를 호출하고 데이터를 요청, 수정할 수 있습니다. 주요 메서드로는 <code>get</code>, <code>post</code>, <code>patch</code>, <code>put</code>, <code>delete</code> 등이 있습니다.</p>
<p>먼저 가상환경을 생성하고  패키지를 설치해줍니다.</p>
<pre><code class="language-bash">python -m venv venv

source venv/scripts/activate

pip install requests</code></pre>
<h1 id="3-requests-method-with-fastapi">3. Requests method with FastAPI</h1>
<p>FastAPI는 빠르고 현대적인 Python 웹 프레임워크로, requests 라이브러리와 함께 사용하면 효율적인 웹 개발이 가능합니다.</p>
<p>이를 간단한 FastAPI 서버를 만들어서 테스트 해보겠습니다.</p>
<p>FastAPI 패키지를 설치해줍니다.</p>
<pre><code class="language-bash">pip install &quot;fastapi[all]&quot;</code></pre>
<p>FastAPI를 이용하여 간단한 서버를 만들어보겠습니다. <code>server.py</code> 파일을 생성하고 다음의 코드를 작성합니다.</p>
<h2 id="3-1-get">3-1. GET</h2>
<pre><code class="language-python">from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get(&quot;/&quot;)
def read_root():
    return {&quot;Hello&quot;: &quot;World&quot;}


@app.get(&quot;/items/{item_id}&quot;)
def read_item(item_id: int, q: Union[str, None] = None):
    return {&quot;item_id&quot;: item_id, &quot;q&quot;: q}</code></pre>
<p>이제 클라이언트에서 해당 서버에 요청을 보내보겠습니다. <code>client.py</code> 파일을 생성하고 아래의 코드를 작성합니다.</p>
<pre><code class="language-python">import requests

url = &quot;http://localhost:8000&quot;

resp = requests.get(url=url)

print(f&quot;status_code: {resp.status_code}&quot;)
print(f&quot;content: {resp.content}&quot;)
print(f&quot;headers: {resp.headers}&quot;)</code></pre>
<p>출력결과는 아래와 같습니다.</p>
<ul>
<li><code>resp.status_code</code>는 말 그대로 HTTP 상태 코드를 반환합니다</li>
<li><code>resp.content</code>는 바이너리 데이터 값을 반환합니다.</li>
<li><code>resp.headers</code>는 response의 메타 데이터를 반환합니다.</li>
</ul>
<pre><code class="language-bash">status_code: 200
content: b&#39;{&quot;Hello&quot;:&quot;World&quot;}&#39;
headers: {&#39;date&#39;: &#39;Fri, 08 Dec 2023 13:09:06 GMT&#39;, &#39;server&#39;: &#39;uvicorn&#39;, &#39;content-length&#39;: &#39;17&#39;, &#39;content-type&#39;: &#39;application/json&#39;}</code></pre>
<p>또한 <code>get</code> 방식으로 HTTP 요청을 할 경우 query string을 통해 응답받을 데이터를 필터링하는 경우가 많습니다.</p>
<p>이는 <code>get</code>의 <code>params</code> 인자로 넘겨주면 query string을 지정할 수 있습니다.</p>
<pre><code class="language-python">import requests

url = f&quot;http://localhost:8000/items/1&quot;

resp = requests.get(url=url, params={&quot;q&quot;: 1})

print(f&quot;status_code: {resp.status_code}&quot;)
print(f&quot;content: {resp.content}&quot;)
print(f&quot;headers: {resp.headers}&quot;)</code></pre>
<pre><code class="language-bash">status_code: 200
content: b&#39;{&quot;item_id&quot;:1,&quot;q&quot;:&quot;1&quot;}&#39;
headers: {&#39;date&#39;: &#39;Fri, 08 Dec 2023 13:16:03 GMT&#39;, &#39;server&#39;: &#39;uvicorn&#39;, &#39;content-length&#39;: &#39;21&#39;, &#39;content-type&#39;: &#39;application/json&#39;}</code></pre>
<p>만약 <code>params</code>를 쓰지 않는다면 url에 그대로 모든 query string을 넣어줘도 무관합니다.</p>
<pre><code class="language-python">...
url = f&quot;http://localhost:8000/items/1?q=1&quot;
...</code></pre>
<h2 id="3-2-post">3-2. POST</h2>
<p>이제 HTTP 요청에 데이터를 담아서 보내는 방식인 <code>post</code>에 대해서 살펴보겠습니다. Requests의 <code>post</code>는 <code>data</code>와 <code>json</code> 옵션으로 데이터를 담아서 보낼 수 있습니다. </p>
<p><code>data</code> 옵션은 HTML 양식(form) 포맷의 데이터를 전송할 수 있고 <code>content-type</code> 요청 헤더는 <code>application/x-www-form-urlencoded</code>로 설정됩니다.
<code>json</code> 옵션은 REST API 형식으로 JSON 포맷의 데이터를 전송할 수 있으며 <code>content-type</code> 요청 헤더는 <code>application/json</code>으로 설정됩니다.</p>
<p><code>server.py</code> 를 아래와 같이 변경해줍니다. 2개 이상의 데이터를 받기 위해 <code>pydantic</code>의 <code>BaseModel</code>을 상속한 클래스를 생성해줍니다.</p>
<pre><code class="language-python">from pydantic import BaseModel
from fastapi import FastAPI

app = FastAPI()


class UserData(BaseModel):
    user_name: str
    password: str


@app.post(&quot;/login&quot;)
def login(user_data: UserData):
    results = {&quot;user_data&quot;: user_data}
    return results</code></pre>
<p>이후 <code>client.py</code> 코드를 아래와 같이 변경 후 <code>data</code> 옵션을 이용해 요청해보겠습니다.</p>
<pre><code class="language-python">import requests

url = f&quot;http://localhost:8000/login&quot;

data = {
    &quot;user_name&quot;: &quot;earthquake&quot;,
    &quot;password&quot;: &quot;pass123word123&quot;
}

resp = requests.post(url=url, data=data)

print(f&quot;status_code: {resp.status_code}&quot;)
print(f&quot;content: {resp.content}&quot;)
print(f&quot;headers: {resp.headers}&quot;)</code></pre>
<p><code>data</code> 옵션을 이용해 요청했더니 아래와 같은 에러를 발생시킵니다. 이유는 FastAPI는 별다른 양식 필드(form)을 지정하지 않으면 JSON으로 요청을 해야하기 때문입니다.</p>
<pre><code class="language-bash">status_code: 422
content: b&#39;{&quot;detail&quot;:[{&quot;type&quot;:&quot;model_attributes_type&quot;,&quot;loc&quot;:[&quot;body&quot;],&quot;msg&quot;:&quot;Input should be a valid dictionary or object to 
extract fields from&quot;,&quot;input&quot;:&quot;user_name=earthquake&amp;password=pass123word123&quot;,&quot;url&quot;:&quot;https://errors.pydantic.dev/2.5/v/model_attributes_type&quot;}]}&#39;
headers: {&#39;date&#39;: &#39;Fri, 08 Dec 2023 13:34:56 GMT&#39;, &#39;server&#39;: &#39;uvicorn&#39;, &#39;content-length&#39;: &#39;255&#39;, &#39;content-type&#39;: &#39;application/json&#39;}</code></pre>
<p>옵션을 다시 <code>json</code>으로 변경한 뒤 실행하면 정상적인 응답이 오는 것을 확인할 수 있습니다. <code>json</code> 옵션을 이용하면 dictionary로 들어온 객체도 JSON의 형태로 변환 후 요청이 됩니다.</p>
<pre><code class="language-python">...
resp = requests.post(url=url, json=data)
...</code></pre>
<pre><code class="language-bash">status_code: 200
content: b&#39;{&quot;user_data&quot;:{&quot;user_name&quot;:&quot;earthquake&quot;,&quot;password&quot;:&quot;pass123word123&quot;}}&#39;
headers: {&#39;date&#39;: &#39;Fri, 08 Dec 2023 13:35:05 GMT&#39;, &#39;server&#39;: &#39;uvicorn&#39;, &#39;content-length&#39;: &#39;68&#39;, &#39;content-type&#39;: &#39;application/json&#39;}</code></pre>
<h1 id="4-프로젝트-적용-사례">4. 프로젝트 적용 사례</h1>
<p>아래는 <a href="https://github.com/earthquakoo/jobdam">Jobdam</a>프로젝트에서 requests 모듈을 이용해 HTTP 통신을 적용한 사례입니다. <code>create_chat_room</code> 이라는 함수에 dictionary 데이터를 인자로 받아 해당 endpoint에 데이터를 전송하고 지정된 리턴값을 받습니다.</p>
<pre><code class="language-python">def create_chat_room(self, data: dict):
    resp = requests.post(
        url=cfg.base_url + &quot;/chat_room/create&quot;,
        json=data,
        headers=auth_utils.build_jwt_header(cfg.config_path)
    )
    if resp.status_code == 201:
        create_room_resp = global_utils.bytes2dict(resp.content)
        return {&quot;status_code&quot;: resp.status_code}
    elif resp.status_code == 400:
        detail = global_utils.bytes2dict(resp.content)[&#39;detail&#39;]
        return {&quot;status_code&quot;: resp.status_code, &quot;detail&quot;: detail}
    else:
        detail = global_utils.bytes2dict(resp.content)[&#39;detail&#39;]
        return {&quot;status_code&quot;: resp.status_code, &quot;detail&quot;: detail}</code></pre>
<p>여기서 주목해야할 점은 5번째 줄에 <code>headers</code>입니다. </p>
<p>제가 만든 app에서는 해당 요청을 보내는 사용자가 인증이 된 사용자인지를 확인해야합니다. 그러기 위해선 HTTP Authorization request header를 <code>post</code> 요청의 <code>headers</code>에 실어서 보내야합니다. HTTP Authorization request header의 양식은 다음과 같습니다. </p>
<p><code>Authorization: &lt;type&gt; &lt;credentials&gt;</code></p>
<ul>
<li><code>type</code>은 인증 타입 혹은 인증 스키마라고도 불리며 대표적인 예로는 <code>Bearer</code> 와 <code>jwt</code>가 있습니다.</li>
<li><code>credentials</code>는 base64 형태로 인코딩되는 것으로 흔히 불리는 <code>access token</code>이 이에 해당합니다.</li>
</ul>
<p><code>auth_utils.build_jwt_header(cfg.config_path)</code>의 함수를 보면 HTTP Authorization request header의 양식을 반환하는 것을 알 수 있습니다. </p>
<pre><code class="language-python">def build_jwt_header(fpath):
    return {
        &quot;Authorization&quot;: &quot;Bearer &quot; + get_access_token_from_json(fpath)
    }</code></pre>
<p>이를 통해 요청을 하는 사용자가 인증된 사용자라면 정상적인 응답을 받을 수 있고 만약 인증된 사용자가 아니라면 아래와 같이 인증되지 않았다라는 응답을 받게 됩니다.</p>
<pre><code class="language-bash">&quot;GET /chat_room/all_rooms_list HTTP/1.1&quot; 401 Unauthorized</code></pre>
<p>또한 8번째 줄을 보면 <code>resp.content</code> 를 <code>global_utils.byes2dict()</code> 라는 함수에 인자로 넘겨준 것을 확인할 수 있습니다.</p>
<p><code>global_utils.byes2dict</code> 함수는 아래와 같이 바이너리 형태로 넘어오는 <code>resp.content</code>를 dictionary 객체로 변환해주는 역할을 합니다.</p>
<pre><code class="language-python">def bytes2dict(b):
    return json.loads(b.decode(&#39;utf-8&#39;))</code></pre>
<p>requests 모듈을 직접 적용한 프로젝트의 코드를 확인하고 싶다면 <a href="https://github.com/earthquakoo/jobdam">Jobdam</a>에 방문해주세요!</p>
<blockquote>
<p>reference</p>
<ul>
<li><a href="https://docs.python-requests.org/en/latest/user/quickstart/">Requests 라이브러리 공식 문서</a></li>
<li><a href="https://www.daleseo.com/python-requests/">파이썬에서 requests 라이브러리로 원격 API 호출하기</a></li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 프로젝트 배포:ECR repository 업로드]]></title>
            <link>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACECR-repository-%EC%97%85%EB%A1%9C%EB%93%9C</link>
            <guid>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACECR-repository-%EC%97%85%EB%A1%9C%EB%93%9C</guid>
            <pubDate>Wed, 13 Dec 2023 15:49:56 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p>이전 포스팅에선 docker를 통해 이미지를 빌드했습니다. 이번에는 Docker hub와 같은 컨테이너 임지에 대한 레지스트리인 AWS ECR(Elastic Container Registry)에 컨테이너를 업로드하는 과정을 살펴보겠습니다.</p>
<h1 id="2-iam-사용자-생성">2. IAM 사용자 생성</h1>
<p>컨테이너 이미지를 ECR 저장소에 게시하기 위해선 먼저 IAM(Identity and Access Management)에서 사용자를 생성해야합니다.</p>
<p><a href="https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/home">AWS IAM</a> 에 접속해서 &quot;액세스관리&quot;의 사용자를 눌러 사용자 생성을 해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/f459d4b8-0dba-437f-83e1-6e622a5d5b6c/image.PNG" alt=""></p>
<p>사용자 이름을 입력하고 다음을 눌러 &quot;직접 정책 연결&quot;을 선택한 후 다음과 같이<code>AmazonEC2ContainerRegistryFullAccess</code> 와 <code>AmazonECS_FullAccess</code> 정책을 추가합니다</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/ac55e943-4a20-4f2b-92f3-ae804b875cc3/image.PNG" alt=""></p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/54522d4a-4e3b-4df6-b5b3-342b40493745/image.PNG" alt=""></p>
<p>이후 생성을 눌러 사용자를 생성하고 생성된 사용자를 클릭해 &quot;보안 자격 증명&quot;을 클릭합니다. 아래의 액세스 키 생성을 눌러 &quot;Command Line Interface(CLI)&quot;로 액세스 키를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/e38606cd-e640-471d-bb4a-0187020ab83c/image.PNG" alt=""></p>
<p>생성된 액세스 키는 CSV 파일로 다운하거나 자신의 프라이빗 저장소에 저장합니다.</p>
<h1 id="ecr-repository-생성">ECR repository 생성</h1>
<p><a href="https://us-east-1.console.aws.amazon.com/ecr/repositories?region=us-east-1">Amazon ECR</a>로 접속해 리포지토리 생성을 클릭합니다.</p>
<p>다음과 같이 리포지토리의 이름을 정해 리포지토리를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/094696b3-7522-4edf-a5dd-90c218e95c1e/image.PNG" alt=""></p>
<p>생성한 리포지토리에 들어와 &quot;푸시 명령 보기&quot;를 클릭하면 명령어들이 제시되는 것을 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/ec742255-1496-4058-b90c-8df0562255ca/image.PNG" alt=""></p>
<h1 id="4-도커-이미지-ecr-repository에-업로드">4. 도커 이미지 ECR repository에 업로드</h1>
<p>액세스 키의 접근 방식을 Command Line Interface(CLI)로 생성했기 때문에 aws-cli 설치를 필요로 합니다. <a href="Z0P1XTvqsxXGjrYF3baN+SvYu1QljXbEVhmZN71V">AWS-CLI</a> 해당 페이지에서 aws-cli를 다운로드 후 설정을 진행합니다.</p>
<p>터미널에서 아래의 설정해줍니다.</p>
<pre><code class="language-bash">$ aws configure

-&gt; AWS Access Key ID [None]: &lt;Acess Key id&gt;
-&gt; AWS Secret Access Key [None]: &lt;Secret Access Key&gt;
-&gt; Default region name [None]: &lt;Your region name&gt;
-&gt; Default output format [None]:</code></pre>
<p>이후 위에서 제시된 푸시 명령들을 순서대로 하나씩 진행합니다.(도커가 실행되고 있는 상태여야합니다.)</p>
<p>마지막 명령어를 입력하면 ECR repository에 업로드가 끝났다면 새로고침을 버튼을 눌러 업로드된 이미지를 확인합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/fdc94da4-1aff-442e-b645-55b1b9fe2c41/image.PNG" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 프로젝트 배포:Dockerzie]]></title>
            <link>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACDockerzie</link>
            <guid>https://velog.io/@earthquake_woo/FastAPI-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%ACDockerzie</guid>
            <pubDate>Tue, 12 Dec 2023 16:45:57 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p>이 시리즈에선 제가 <a href="https://github.com/earthquakoo/jobdam">Jobdam</a>이란 프로젝트에서 서버를 실제로 어떻게 배포했는지 살펴보겠습니다.</p>
<p>프로젝트를 배포하는 한 가지 방법은 Docker를 사용하여 컨테이너 이미지를 빌드하는 것입니다. 이 글에서는 Docker의 기본 개념과 함께 FastAPI 프로젝트를 어떻게 컨테이너 이미지로 만드는지 알아봅니다.</p>
<h1 id="2-docker도커란">2. Docker(도커)란?</h1>
<p>도커는 소프트웨어를 <strong>컨테이너(Container)</strong> 라는  표준화된 유닛으로 패키징하여 애플리케이션을 구축하고 배포할 수 있게 해주는 오픈소스 프로젝트입니다. </p>
<p>간단하게 말하면, 애플리케이션의 코드와 설치된 라이브러리 등을 컨테이너라는 공간에 담아서 배포하기 쉽게 도와주는 플랫폼입니다.</p>
<p>그렇다면 컨테이너란 무엇일까요</p>
<h2 id="2-1-컨테이너container">2-1. 컨테이너(Container)</h2>
<p>흔히 말하는 소프트웨어는 OS와 라이브러리에 의존성을 가집니다. 만약 서로 다른 OS나 버전이 다른 라이브러리의 소프트웨어를 실행하고자 할 때 오류를 가져올 수 있고 이를 제어하기가 어려워집니다.</p>
<p>컨테이너는 소프트웨어(애플리케이션)를 실행할 때 독립적인 환경을 제공해주는 운영체제 수준의 격리 기술을 말합니다. </p>
<p>설명을 위해 가상 머신과 컨테이너를 비교해보겠습니다. 아래 사진과 같이 가상 머신은 OS 레벨을 포함한 전체를 가상화하는 가상머신과 달리 격리된 프로세스로 실행되는 컨테이너는 가상머신에 비해 매우 가볍습니다.</p>
<p>쉽게 얘기하면 window에서 개발하는 사람과 mac에서 개발하는 사람은 OS가 다릅니다. 또한 사용하는 언어의 버전이나 라이브러리 버전 등이 다를 수 있는 것을 방지하고자 컨테이너를 사용하여 동일한 OS단에서 코드와 라이브러리 버전의 통일성을 만드는 것입니다.</p>
<p>여기서 컨테이너 실행에 필요한 프로그램과 소스코드, 라이브러리 등을 포함시킨 것 파일을 Docker image(도커 이미지)라고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/e46c944e-f85e-4982-b24f-d236292635c1/image.png" alt=""></p>
<h2 id="2-2-도커-이미지docker-image">2-2. 도커 이미지(Docker image)</h2>
<p>도커 이미지는 서비스 운영에 필요한 프로그램, 소스코드, 라이브러리 등을 포함한 파일입니다. 즉, 특정 프로세스가 실행되기 위한(여기선 컨테이너 실행) 모든 파일과 환경을 지닌 것으로 더 이상의 의존성 파일을 컴파일하거나 라이브러리를 설치할 필요가 없는 상태의 파일을 의미합니다.</p>
<p>이미지는 읽기 전용(read-only)으로 스냅샷이라고도 합니다. 따라서 이미지는 실행할 수 없는 <strong>정적 이미지(불변성)</strong> 입니다. 즉, 이미지는 빌드 타임 구조로 이루어져 있고 컨테이너는 런타임 구조로 실행 중인 이미지를 나타냅니다.(예를 들면 객체 지향 언어에서의 클래스와 해당 클래스 객체의 차이인 것 같습니다?)</p>
<h2 id="2-3-레이어layer">2-3. 레이어(Layer)</h2>
<p>도커 이미지는 컨테이너를 생성하기 위한 모든 정보를 가지고 있기 때문에 보통 수백MB ~ 수GB가 넘는다. 그런데 기존 이미지에서 작은 변경사항이 생겨 도커 파일에 코드 몇 줄을 추가해 다시 이미지를 만들고 다시 그 이미지를 다운 받는다고 가정합니다. </p>
<p>이때 이미지의 불변성 때문에 수백MB ~ 수백GB가 되는 이미지를 다시 다운로드 받는 것은 매우 비효율적입니다. 도커는 이러한 문제를 해결하기 위해 레이어(Layer)라는 개념을 도입했습니다.</p>
<p>레이어란 수정사항이나 추가적인 파일이 있을 때 다시 다운로드 받는 것이 아닌 파일을 추가하는 개념입니다. 아래의 사진과 같이 기존의 이미지가 존재하는데 새로운 이미지를 다운 받을 경우 레이어만 따로 빼 파일을 추가하는 형식입니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/a1063e20-e61a-45e9-8e29-860b3edceb50/image.png" alt=""></p>
<p>도커 이미지는 위 그림처럼 여러 개의 읽기 전요(read-only) 레이어로 구성되고, 파일이 추가되면 새로운 레이어가 생성됩니다. 이러한 개념은 git 레포지토리에 commit 로그를 쌓는 것과 같다고 볼 수 있습니다.</p>
<h1 id="3-프로젝트-생성">3. 프로젝트 생성</h1>
<p>이제 이론을 바탕으로 실전에 적용해볼 차례입니다. 간단한 FastAPI 프로젝트를 생성하고 docker를 이용하여 컨테이너 이미지를 빌드해보겠습니다.</p>
<p>새로운 폴더를 만들고 가상환경 생성 후 활성화합니다.</p>
<pre><code class="language-bash">python -m venv venv

source venv/scripts/activate</code></pre>
<p>이후 필요한 라이브러리를 설치하고 종속성을 파일에 저장합니다.</p>
<pre><code class="language-bash">pip install &quot;fastapi[all]&quot;

pip freeze &gt; requirements.txt</code></pre>
<p> <code>app</code> 폴더를 생성 후 해당 폴더에 <code>main.py</code> 파일을 만들고 루트 도메인에 간단한 문구를 출력하는 FastAPI 서버 코드를 작성해보겠습니다.</p>
<pre><code class="language-python">from fastapi import FastAPI

app = FastAPI()

@app.get(&#39;/&#39;)
def welcome_root():
    return &quot;Welcome to root&quot;</code></pre>
<p>다음과 같은 명령어로 실행하면 서버가 잘 동작하는 것을 확인할 수 있습니다.</p>
<pre><code class="language-bash">uvicorn app.main:app --reload</code></pre>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/ae1cea3f-77de-43b1-a196-ed81970d36bf/image.PNG" alt=""></p>
<h1 id="4-dockerfile-생성">4. Dockerfile 생성</h1>
<p>루트 디렉토리에 <code>Dockerfile</code>을 생성하고 다음과 같이 입력합니다.</p>
<pre><code class="language-Dockerfile">FROM python:3.11

WORKDIR /test

COPY ./requirements.txt /test/requirements.txt

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

COPY ./app /test/app

CMD [&quot;uvicorn&quot;, &quot;app.main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]</code></pre>
<p>여기서 중요한 것은 다음과 같은 코드입니다.</p>
<pre><code class="language-Dockerfile">COPY ./requirements.txt /test/requirements.txt</code></pre>
<p>먼저 코드의 나머지 부분이 아닌 종속성을 모아둔 파일을 dockerfile에 복사합니다. 이것은 위에선 언급했던 레이어 개념이 적용된 부분입니다.</p>
<p>도커는 컨테이너 이미지를 점진적으로 구축하여 수정사항이나 새로운 파일이 추가되면 기존의 레이어에 추가되어 구축됩니다. 이때 도커가 수정되지 않은 레이어를 발견하면 캐시에서 해당 레이어를 재사용하여 시간을 절약할 수 있습니다.</p>
<p>즉, 종속성을 저장하는 파일인 <code>requirements.txt</code>는 소스코드에 비해 자주 수정되는 파일이 아니기 때문에 소스 파일 위에 종속성 파일을 설치하는 명령어를 배치하는 것이 더 효율적입니다.</p>
<p>모든 작업이 끝났다면 프로젝트 구조는 다음과 같습니다.</p>
<pre><code>.
├── app
│   ├── __init__.py
│   └── main.py
├── Dockerfile
└── requirements.txt</code></pre><h1 id="5-docker-이미지-빌드">5. Docker 이미지 빌드</h1>
<p>이제 도커 이미지를 빌드하고 실행합니다.</p>
<pre><code class="language-bash">docker build -t test-image:latest .</code></pre>
<p>빌드된 시간을 보면 23.9초가 걸린 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/b5557892-99aa-4ff7-9e50-67356a53010d/image.PNG" alt=""></p>
<p>이후 소스코드를 변경한 후 다시 이미지를 빌드해보겠습니다.</p>
<pre><code class="language-python">from fastapi import FastAPI

app = FastAPI()

@app.get(&#39;/&#39;)
def welcome_root():
    return &quot;Hello world!&quot;</code></pre>
<p>중간에 <code>CACHED</code> 라는 명령어 줄을 확인해보면 캐시가 된 것을 확인할 수 있고 빌드 시간이 16.5초로 확실히 감소된 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/326f9570-3190-4fc5-8d51-69111d943a81/image.PNG" alt=""></p>
<p>만약 방금 생성한 이미지에서 컨테이너를 실행하려면 다음과 같은 명령어로 실행합니다.</p>
<pre><code class="language-bash">docker run -p 8000:8000 test-image</code></pre>
<blockquote>
<p>reference</p>
</blockquote>
<ul>
<li><a href="https://hub.docker.com/_/python">Docker python</a></li>
<li><a href="https://docs.docker.com/?_gl=1*1g2xro4*_ga*MjEwMTQ0MDI0Ny4xNzAxODc0MTEx*_ga_XJWPQMJYHQ*MTcwMjM5ODk4My40LjEuMTcwMjM5OTAzNS44LjAuMA..">Docker reference</a></li>
<li><a href="https://fastapi.tiangolo.com/deployment/docker/">FastAPI-Docker</a></li>
<li><a href="https://noisrucer.github.io/posts/dockerize/">https://noisrucer.github.io/posts/dockerize/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python Textual 라이브러리로 TUI App 개발:배포]]></title>
            <link>https://velog.io/@earthquake_woo/Python-Textual-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-TUI-App-%EA%B0%9C%EB%B0%9C%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@earthquake_woo/Python-Textual-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-TUI-App-%EA%B0%9C%EB%B0%9C%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Tue, 12 Dec 2023 11:18:45 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p>이전 포스팅에서 <code>textual</code>을 이용해 간단한 TUI app을 만들어보았습니다. 이번 포스팅에서는 이 라이브러리를 사용자들이 쉽게 활용할 수 있도록 배포하는 방법에 대해 알아보겠습니다.</p>
<h1 id="2-환경설정">2. 환경설정</h1>
<p>먼저 <code>poetry를</code> 사용하여 <code>PyPI</code>에 배포하기 위한 환경을 설정합니다.</p>
<pre><code class="language-bash">pip install pipx

pipx install poetry</code></pre>
<p>이후 바탕화면에서 새로운 poetry 프로젝트를 생성합니다.</p>
<pre><code class="language-bash">poetry new textual_app</code></pre>
<p>생성한 poetry 프로젝트로 이동하여 코드를 작성할 편집기로 폴더를 열어줍니다! (예: vscode)</p>
<pre><code>cd textual-app
code .</code></pre><p>프로젝트의 구조는 다음과 같습니다.</p>
<pre><code>&gt; tests
&gt; textual_app
pyproject.toml
README.md</code></pre><h1 id="3-textual-app-생성">3. Textual app 생성</h1>
<p>간단한 설정이 완료되었으므로 textual app을 만들어보겠습니다. 먼저 필요한 라이브러리를 설치합니다.</p>
<pre><code class="language-bash">poetry add &quot;textual[dev]&quot;</code></pre>
<p>설치된 패키지는 <code>pyproject.toml</code>의 dependencies에 추가됩니다. <code>pyproject.toml</code>은 Python 프로젝트의 빌드 시스템 요구 사항을 담은 파일입니다.</p>
<pre><code class="language-toml">[tool.poetry]
name = &quot;textual-app&quot;
version = &quot;0.1.0&quot;
description = &quot;&quot;
authors = [&quot;Earthquakoo &lt;cream5343@gmail.com&gt;&quot;]
readme = &quot;README.md&quot;

[tool.poetry.dependencies]
python = &quot;^3.9&quot;
textual = {extras = [&quot;dev&quot;], version = &quot;^0.44.1&quot;}


[build-system]
requires = [&quot;poetry-core&quot;]
build-backend = &quot;poetry.core.masonry.api&quot;
</code></pre>
<p>필요한 라이브러리가 설치되었다면 poetry 가상환경을 실행합니다.</p>
<pre><code class="language-bash">poetry shell</code></pre>
<p>이제 저번 포스팅에서 만들었던 코드를 가져와 <code>textual_app</code> 폴더에서 <code>main.py</code> 파일을 생성하고 붙여넣기 해줍니다.</p>
<pre><code class="language-python">from textual import on
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.containers import Container, Vertical
from textual.widgets import Input, RichLog, Button


class HomeScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Container(
            Button(&quot;Get Started&quot;, id=&quot;get_started&quot;, classes=&quot;button_widget&quot;),
            Button(&quot;Exit&quot;, id=&quot;exit&quot;, classes=&quot;button_widget&quot;),
            classes=&quot;home_screen_container&quot;,
        )

    @on(Button.Pressed)
    def button_pressed_handler(self, event: Button.Pressed) -&gt; None:
        if event.button.id == &quot;get_started&quot;:
            self.app.push_screen(ChatScreen())
        elif event.button.id == &quot;exit&quot;:
            self.app.exit()


class ChatScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Vertical(
            RichLog(classes=&quot;richlog_widget&quot;),
            Input(placeholder=&quot;Enter chat&quot;, classes=&quot;input_widget&quot;),
            classes=&quot;vertical_layout&quot;
        )

    @on(Input.Submitted)
    def input_submitted_handler(self, event: Input.Submitted):
        log = self.query_one(RichLog)
        log.write(f&quot;earthquake: {event.value}&quot;)
        input = self.query_one(Input)
        input.value = &quot;&quot;    



class ChatApp(App):
    CSS_PATH = &quot;main.css&quot;

    def on_mount(self) -&gt; None:
        self.app.push_screen(HomeScreen())


if __name__  == &quot;__main__&quot;:
    app=ChatApp()
    app.run()</code></pre>
<p>위와 같은 예시 프로젝트를 실행하기 위해서 터미널에 아래와 같이 입력했습니다. 하지만 이것은 사용들이 사용하기엔 조금 불편함이 있습니다. 이를 수정해보도록 하겠습니다.</p>
<pre><code class="language-bash">python textual_app/main.py</code></pre>
<h1 id="4-패키지-관리">4. 패키지 관리</h1>
<p>이제 <code>pyproject.toml</code>에서 몇 가지를 수정해야 합니다.</p>
<ul>
<li><code>version</code>은 0.1.0으로 초기화 되어있습니다.</li>
<li><code>description</code>은 app에 대한 간단한 설명을 추가할 수 있습니다.</li>
<li><code>[tool.poetry.scripts]</code>로 패키지를 실행하는 명령을 정의할 수 있습니다.</li>
</ul>
<p>여기서 중요한 것은 <code>[tool.poetry.scripts]</code> 입니다. 이전에 프로젝트를 실행하기 위해 터미널에서 <code>python textual_app/main.py</code> 로 실행했습니다. 그러나 이것은 조금 번거로우며 사용자가 길게 늘어진 패키지 타이핑을 실행하는 것은 좋지 못한 예입니다.</p>
<p>따라서 해당 패키지를 설치했을 때 간단한 타이핑으로 이 app을 실행할 수 있도록 <code>scripts를</code> 추가합니다.</p>
<pre><code class="language-toml">[tool.poetry]
name = &quot;textual-app&quot;
version = &quot;0.1.0&quot;
description = &quot;Simple textual app&quot;
authors = [&quot;Earthquakoo &lt;cream5343@gmail.com&gt;&quot;]
readme = &quot;README.md&quot;

[tool.poetry.dependencies]
python = &quot;^3.9&quot;
textual = {extras = [&quot;dev&quot;], version = &quot;^0.44.1&quot;}

[tool.poetry.scripts]
textual-app = &quot;textual_app.main:main&quot;

[build-system]
requires = [&quot;poetry-core&quot;]
build-backend = &quot;poetry.core.masonry.api&quot;</code></pre>
<p><code>scripts를</code> 설정했다면 <code>main.py</code>에서 해당 app의 경로를 정의한 것을 삭제하고 패키지를 실행하는 함수를 생성합니다.</p>
<pre><code class="language-python">from textual import on
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.containers import Container, Vertical
from textual.widgets import Input, RichLog, Button


class HomeScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Container(
            Button(&quot;Get Started&quot;, id=&quot;get_started&quot;, classes=&quot;button_widget&quot;),
            Button(&quot;Exit&quot;, id=&quot;exit&quot;, classes=&quot;button_widget&quot;),
            classes=&quot;home_screen_container&quot;,
        )

    @on(Button.Pressed)
    def button_pressed_handler(self, event: Button.Pressed) -&gt; None:
        if event.button.id == &quot;get_started&quot;:
            self.app.push_screen(ChatScreen())
        elif event.button.id == &quot;exit&quot;:
            self.app.exit()


class ChatScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Vertical(
            RichLog(classes=&quot;richlog_widget&quot;),
            Input(placeholder=&quot;Enter chat&quot;, classes=&quot;input_widget&quot;),
            classes=&quot;vertical_layout&quot;
        )

    @on(Input.Submitted)
    def input_submitted_handler(self, event: Input.Submitted):
        log = self.query_one(RichLog)
        log.write(f&quot;earthquake: {event.value}&quot;)
        input = self.query_one(Input)
        input.value = &quot;&quot;    



class ChatApp(App):
    CSS_PATH = &quot;main.css&quot;

    def on_mount(self) -&gt; None:
        self.app.push_screen(HomeScreen())


def main():
    app = ChatApp()
    app.run()


# if __name__ == &quot;__main__&quot;:
#     app = ChatApp()
#     app.run()</code></pre>
<h1 id="5-패키지-테스트-및-배포">5. 패키지 테스트 및 배포</h1>
<p>테스트를 위해 로컬에서 패키지를 설치하고 실행해봅니다.</p>
<pre><code class="language-bash">poetry install</code></pre>
<p>패키지가 제대로 설치되었다면 아래의 <code>textual-app</code> 명령어로 간단히 패키지를 실행할 수 있습니다!</p>
<pre><code class="language-bash">textual-app</code></pre>
<p>이제 다른 사람들이 해당 패키지를 다운로드할 수 있도록 PyPI에 배포를 해보겠습니다.</p>
<pre><code class="language-bash">poetry build</code></pre>
<p><code>poetry build</code> 명령어를 실행하면 프로젝트 구조는 아래와 같아집니다.</p>
<pre><code>&gt; dist
&gt; tests
&gt; textual_app
pyproject.toml
README.md</code></pre><p>이제 <a href="https://pypi.org/">PyPI</a>사이트에서 계정을 생성하고 아래와 같이 <a href="https://pypi.org/manage/account/token/">API 토큰</a>을 등록해주고 발급 받은 토큰은 따로 저장해둡니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/19cfaad9-5cc4-468f-929a-d89676a899c0/image.png" alt=""></p>
<p>아래의 커맨드와 함께 저장한 토큰을 입력합니다.</p>
<pre><code class="language-bash">poetry config pypi-token.pypi &lt;Your api token&gt;</code></pre>
<p>이제 패키지를 배포합니다.</p>
<pre><code class="language-bash">poetry publish --build</code></pre>
<p><a href="https://pypi.org/">PyPI</a>에서 자신의 패키지명을 검색하면 배포가 된 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/67552338-e30c-47e5-a94b-4cee652a5430/image.png" alt=""></p>
<p>터미널에서 배포한 패키지를 설치해서 테스트합니다.</p>
<pre><code class="language-bash">pip install textual-app

textual-app</code></pre>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/c38b276d-1a12-41ba-b1f6-2a476b9b6ac8/image.PNG" alt=""></p>
<h1 id="6-패키지-업데이트">6. 패키지 업데이트</h1>
<p>만약 수정할 것이 있거나 새로운 기능이 생겨 업데이트가 필요하다면 <code>pyproject.toml</code>에서 <code>version</code>과 <code>description</code>을 수정합니다.</p>
<pre><code class="language-toml">[tool.poetry]
name = &quot;textual-app&quot;
version = &quot;0.1.1&quot;
description = &quot;Update textual app&quot;
authors = [&quot;Earthquakoo &lt;cream5343@gmail.com&gt;&quot;]
readme = &quot;README.md&quot;

[tool.poetry.dependencies]
python = &quot;^3.9&quot;
textual = {extras = [&quot;dev&quot;], version = &quot;^0.44.1&quot;}

[tool.poetry.scripts]
textual-app = &quot;textual_app.main:main&quot;

[build-system]
requires = [&quot;poetry-core&quot;]
build-backend = &quot;poetry.core.masonry.api&quot;
</code></pre>
<p>또한 <code>textual_app/__init__.py</code>의 <code>__version__</code>도 함께 수정합니다.</p>
<pre><code class="language-python">__version__ = &quot;0.1.1&quot;</code></pre>
<p>다시 아래의 커맨드로 업데이트된 패키지를 배포합니다.</p>
<pre><code class="language-bash">poetry publish --build</code></pre>
<p>이후 패키지를 업데이트하려면 다음을 실행합니다.</p>
<pre><code class="language-bash">pip install textual-app --upgrade</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python Textual 라이브러리로 TUI App 개발:프로젝트 활용]]></title>
            <link>https://velog.io/@earthquake_woo/Python-Textual-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-TUI-App-%EA%B0%9C%EB%B0%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@earthquake_woo/Python-Textual-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-TUI-App-%EA%B0%9C%EB%B0%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Tue, 12 Dec 2023 11:11:38 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p>이전 포스팅에선 간단한 TUI app을 만들었습니다. 하지만 이는 너무 간단해보입니다. 그렇기에 조금 심화과정으로 저의 <a href="https://github.com/earthquakoo/jobdam">Jobdam</a> 에서 적용되었던 몇 가지 기능을 추가해 TUI app을 꾸며보도록 하겠습니다.</p>
<h1 id="2-screen">2. Screen</h1>
<p>Textual에서 <a href="https://textual.textualize.io/guide/screens/">Screen</a>은 터미널의 크기를 차지하는 위젯의 컨테이너입니다. 특정 앱에는 여러 개의 화면이 있을 수 있지만 한 번에 하나의 화면만 활성화됩니다.</p>
<p>먼저 앱을 실행했을 때 맨 처음 보이는 화면을 만들어보겠습니다.</p>
<p><code>from textual.screen</code> 에서 <code>Screen</code> 위젯 컨테이너를 가져와 특정 화면을 만들 수 있습니다.</p>
<pre><code class="language-python">from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Welcome

class HomeScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Welcome()</code></pre>
<p>화면을 터미널 상에 출력하고 싶을 때는 <code>push_screen</code>이라는 메소드를 이용해 화면을 출력할 수 있습니다. </p>
<pre><code class="language-python">...
class ChatApp(App):

    def on_mount(self) -&gt; None:
        self.app.push_screen(HomeScreen())


if __name__  == &quot;__main__&quot;:
    app=ChatApp()
    app.run()</code></pre>
<p><code>on_mount</code> 메소드를 이용해 앱이 실행될 때 가장 먼저 마운트되는 것을 <code>push_screen</code>으로 설정한 뒤 앱을 실행하면 다음과 같은 결과를 얻을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/10b174b2-60a5-431f-b5f2-2eddaf219878/image.PNG" alt=""></p>
<h2 id="2-1-homescreen">2-1. HomeScreen</h2>
<p>Textual은 빠른 스타일링을 위해 여러 컨테이너 위젯을 지원합니다. </p>
<ul>
<li><code>Horizontal</code>: 가로 레이아웃을 위한 컨테이너</li>
<li><code>Vertical</code>: 세로 레이아웃을 위한 컨테이너</li>
<li><code>Grid</code>: 그리드 레이아웃을 위한 컨테이너</li>
<li><code>Container</code>: 수직 레이아웃을 위한 컨테이너</li>
</ul>
<p>위는 대표적인 textual의 컨테이너 위젯이며 <a href="https://textual.textualize.io/api/containers/">Textaul container</a>공식문서에서 더 많은 컨테이너를 확인할 수 있습니다.</p>
<p>저는 홈화면에서 <code>container</code> 위젯으로 수직 레이아웃을 만들고 컨테이너 내에 위젯의 css 스타일을 적용하기 위해 <code>classes</code>와 css 파일인 <code>main.css</code> 만들어주었습니다.</p>
<p><code>Button</code>의 <code>classes</code> 또한 위젯의 스타일을 지정하는 것이고 <code>id</code>는 나중에 상호작용을 위해 쓰일 예정입니다.</p>
<p>그리고 이 앱에 css 파일을 적용하기 위해선 <code>CSS_PATH</code>에 경로를 제공하면 됩니다.</p>
<pre><code class="language-python">from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.containers import Container
from textual.widgets import Button

class HomeScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Container(
            Button(&quot;Get Started&quot;, id=&quot;get_started&quot;, classes=&quot;button_widget&quot;),
            Button(&quot;Exit&quot;, id=&quot;exit&quot;, classes=&quot;button_widget&quot;),
            classes=&quot;home_screen_container&quot;,
        )

class ChatApp(App):
    CSS_PATH = &quot;main.css&quot;

    def on_mount(self) -&gt; None:
        self.app.push_screen(HomeScreen())</code></pre>
<p>위젯의 css 스타일을 적용한 방식이 더 궁금하다면 <a href="https://textual.textualize.io/guide/styles/">Textual css style</a>, <a href="https://textual.textualize.io/guide/CSS/">Textual text css</a>를 참고해주세요</p>
<pre><code class="language-css">HomeScreen {
    background: black;
}

HomeScreen .home_screen_container {
    align: center middle;
}

HomeScreen .button_widget {
    width: 10%;
    margin: 1 1 1 1;
    border: blank white;
    background: black;
}</code></pre>
<p>위와 같이 설정이 끝났다면 아래와 같은 결과를 얻을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/92228b6c-a113-4d5d-9d17-11ebcf9f0c59/image.PNG" alt=""></p>
<p>하지만 아직 버튼을 눌러도 아무런 반응을 보이지 않습니다. 이것은 후에 설정을 해주겠습니다.</p>
<h2 id="2-2-chatscreen">2-2. ChatScreen</h2>
<p>이제 이전에서 만들었던 간단한 채팅화면을 적용할 차례입니다.</p>
<p>홈화면을 만들었던 것과 비슷하게 <code>Vertical</code> 컨테이너를 이용해 수직 레이아웃을 만들어 그 안에 위젯들을 배치시킵니다.</p>
<pre><code class="language-python">class ChatScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Vertical(
            RichLog(classes=&quot;richlog_widget&quot;),
            Input(placeholder=&quot;Enter chat&quot;, classes=&quot;input_widget&quot;),
            classes=&quot;vertical_layout&quot;
        )</code></pre>
<p>아래는 <code>ChatScreen</code>의 css 구성입니다.</p>
<pre><code class="language-css">ChatScreen {
    background: black;
}

ChatScreen .vertical_layout {
    align: center middle;
    background: black;
}

ChatScreen .input_widget {
    width: 100%;
    margin-top: 1;
    margin-bottom: 1;
    border: white;
}

ChatScreen .richlog_widget {
    align: center middle;
    background: black;
    scrollbar-size-vertical: 1;
    scrollbar-color: white;
    border: white;
}</code></pre>
<p>화면의 구성은 모두 끝이 났습니다. 하지만 아직 버튼이나 인풋의 상호작용이 일어나지 않습니다. 이제 이벤트 핸들러를 통해 상호작용을 할 수 있도록 해보겠습니다.</p>
<h1 id="3-상호작용">3. 상호작용</h1>
<p><code>on</code> 데코레이터를 이용해 이벤트 핸들러로 동작하게 합니다. </p>
<p>만약 <code>event.button.id</code>가 <code>get_started</code>라면 <code>ChatScreen</code>을 화면에 출력하고 <code>event.button.id</code> 가 <code>exit</code>이라면 앱을 종료합니다.</p>
<pre><code class="language-python">from textual import on

class HomeScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Container(
            Button(&quot;Get Started&quot;, id=&quot;get_started&quot;, classes=&quot;button_widget&quot;),
            Button(&quot;Exit&quot;, id=&quot;exit&quot;, classes=&quot;button_widget&quot;),
            classes=&quot;home_screen_container&quot;,
        )

    @on(Button.Pressed)
    def button_pressed_handler(self, event: Button.Pressed) -&gt; None:
        if event.button.id == &quot;get_started&quot;:
            self.app.push_screen(ChatScreen())
        elif event.button.id == &quot;exit&quot;:
            self.app.exit()</code></pre>
<p><code>ChatScreen</code>도 상호작용을 위해 코드를 추가해줍니다.</p>
<pre><code class="language-python">class ChatScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Vertical(
            RichLog(classes=&quot;richlog_widget&quot;),
            Input(placeholder=&quot;Enter chat&quot;, classes=&quot;input_widget&quot;),
            classes=&quot;vertical_layout&quot;
        )

    @on(Input.Submitted)
    def input_submitted_handler(self, event: Input.Submitted):
        log = self.query_one(RichLog)
        log.write(f&quot;earthquake: {event.value}&quot;)
        input = self.query_one(Input)
        input.value = &quot;&quot;    </code></pre>
<p>전체 코드는 다음과 같습니다.</p>
<pre><code class="language-python">from textual import on
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.containers import Container, Vertical
from textual.widgets import Input, RichLog, Button


class HomeScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Container(
            Button(&quot;Get Started&quot;, id=&quot;get_started&quot;, classes=&quot;button_widget&quot;),
            Button(&quot;Exit&quot;, id=&quot;exit&quot;, classes=&quot;button_widget&quot;),
            classes=&quot;home_screen_container&quot;,
        )

    @on(Button.Pressed)
    def button_pressed_handler(self, event: Button.Pressed) -&gt; None:
        if event.button.id == &quot;get_started&quot;:
            self.app.push_screen(ChatScreen())
        elif event.button.id == &quot;exit&quot;:
            self.app.exit()


class ChatScreen(Screen):

    def compose(self) -&gt; ComposeResult:
        yield Vertical(
            RichLog(classes=&quot;richlog_widget&quot;),
            Input(placeholder=&quot;Enter chat&quot;, classes=&quot;input_widget&quot;),
            classes=&quot;vertical_layout&quot;
        )

    @on(Input.Submitted)
    def input_submitted_handler(self, event: Input.Submitted):
        log = self.query_one(RichLog)
        log.write(f&quot;earthquake: {event.value}&quot;)
        input = self.query_one(Input)
        input.value = &quot;&quot;    



class ChatApp(App):
    CSS_PATH = &quot;main.css&quot;

    def on_mount(self) -&gt; None:
        self.app.push_screen(HomeScreen())


if __name__  == &quot;__main__&quot;:
    app=ChatApp()
    app.run()</code></pre>
<p>이제 앱을 실행해 <code>Get_started</code>를 클릭하여 <code>ChatScreen</code>으로 이동하면 다음과 같은 화면으로 간단한 채팅앱이 구현된 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/c97f5ecf-f063-4ae0-b95c-f16980756802/image.PNG" alt=""></p>
<p>다음 포스팅에선 구축한 앱을 PyPI에 배포하는 법을 살펴보겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python Textual 라이브러리로 TUI App 개발:프로젝트 생성]]></title>
            <link>https://velog.io/@earthquake_woo/Python-Textual-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-TUI-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@earthquake_woo/Python-Textual-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-TUI-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%83%9D%EC%84%B1</guid>
            <pubDate>Tue, 12 Dec 2023 09:44:09 GMT</pubDate>
            <description><![CDATA[<h1 id="1-introduction">1. Introduction</h1>
<p>코딩을 하면서 그래픽 환경인 CLI, GUI, TUI에 대해 들어보신 적이 있을 것입니다.</p>
<ul>
<li>CLI (Command Line Interface): 명령어를 입력하여 상호작용하는 환경으로, 윈도우에는 cmd, 리눅스에는 terminal이 대표적입니다.</li>
<li>GUI (Graphic User Interface): 그래픽으로 사용자가 상호작용하는 환경으로, UI(User Interface)에 시각적인 개념이 추가된 용어입니다.</li>
<li>TUI (Text User Interface): 텍스트를 통해 사용자가 상호작용하는 환경으로, 주로 터미널에서 사용되며 리눅스의 vi(vim) 편집기가 대표적입니다. (위키피디아에서는 Text User Interface를 컴퓨터 터미널의 속성에 대한 의존성을 반영하기 위해 Terminal User Interface라고도 언급하고 있습니다.)</li>
</ul>
<p>하지만 TUI는 종종 텍스트가 주를 이루어 한 눈에 들어오지 않아 쉽게 와닿지 않을 수 있습니다. 따라서, 이번에는 <a href="https://textual.textualize.io/">Textual</a>을 활용하여 시각적으로 화려한 TUI 앱을 구축해보려 합니다.</p>
<h1 id="2-project-생성">2. Project 생성</h1>
<p>os: window, terminal: git bash</p>
<pre><code class="language-bash">python -m venv venv
source venv/scripts/activate</code></pre>
<p>아래의 커맨드로 개발용 textual 패키지를 설치해줍니다.</p>
<pre><code class="language-bash">pip install &quot;textual-dev&quot;</code></pre>
<p>만약 textual 패키지 자체를 다운로드하거나 예제를 테스트하고 싶다면 다음 명령어로 textual 패키지를 설치할 수 있습니다. (TUI 앱을 개발할 목적이라면 앞서 언급한 명령어를 사용하세요)</p>
<pre><code class="language-bash">pip install &quot;textual&quot;</code></pre>
<h1 id="3-간단한-tui-app-만들기">3. 간단한 TUI app 만들기</h1>
<p>이제 <code>Textual</code>을 이용해 문구를 입력하면 화면에 출력되는 간단한 TUI app을 만들어보겠습니다.</p>
<pre><code class="language-python">from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Input, RichLog


class ChatApp(App):

    def compose(self) -&gt; ComposeResult:
        yield RichLog()
        yield Input()

    @on(Input.Submitted)
    def on_input_submitted(self, event: Input.Submitted):
        input = self.query_one(Input)
        log = self.query_one(RichLog)
        log.write(f&quot;earthquake: {event.value}&quot;)
        input.value = &quot;&quot;


if __name__  == &quot;__main__&quot;:
    app=ChatApp()
    app.run()</code></pre>
<p><code>compose</code> 메소드는 반복 가능한 인스턴스나 위젯, 혹은 위젯의 목록을 반환합니다. 여기서는 위젯을 생성하여 메서드를 generator로 만드는 것이 편리합니다.</p>
<p>입력을 받기 위해 <code>Input</code> 위젯과 입력받은 결과를 화면에 출력하기 위한 <code>RichLog</code> 위젯을 생성했습니다.</p>
<pre><code class="language-python">...
def compose(self) -&gt; ComposeResult:
    yield RichLog()
    yield Input()</code></pre>
<ul>
<li><code>on_input_submitted는</code> 이벤트 핸들러로, 이벤트 핸들러는 키 누르기, 마우스 클릭 등과 같은 이벤트에 대한 응답으로 textual에서 호출하는 메소드입니다. 여기서는 Input에서 리턴값이 생겼을 때의 이벤트를 의미합니다.<ul>
<li><code>on_input_submitted</code>처럼 함수명을 제시해서 이벤트 핸들러로 사용할 수도 있지만 <code>@on</code> 이라는 데코레이터를 이용해서도 이벤트 핸들러처럼 사용할 수 있습니다. 아래의 두 가지 예시는 동일한 동작을 수행합니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-python">...
@on(Input.Submitted)
def input_submitted_handler(self, event: Input.Submitted):
...</code></pre>
<pre><code class="language-python">...
def on_input_submitted(self, event: Input.Submitted):
...</code></pre>
<ul>
<li><code>query_one</code> 메소드는 generator 또는 유형과 일치하는 단일 위젯을 가져옵니다.</li>
<li>3번째줄에선 <code>RichLog</code> 위젯을 가져와 <code>RichLog</code>의 입력 메소드인 <code>write</code>로 <code>Input</code>에서 전달받은 값을 화면에 출력합니다.</li>
<li>이후 <code>Input</code>의 제출이 완료되면 <code>Input</code> 칸의 value를 초기화시켜줍니다.</li>
</ul>
<pre><code class="language-python">...
input = self.query_one(Input)
log = self.query_one(RichLog)
log.write(f&quot;earthquake: {event.value}&quot;)
input.value = &quot;&quot;
...</code></pre>
<p>실행 결과로 &quot;hello world&quot;를 입력하면 아래와 같은 결과를 얻을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/earthquake_woo/post/16032ee4-940f-43bd-8630-bb0f333c595b/image.PNG" alt=""></p>
<p>더 자세한 정보는 <a href="https://textual.textualize.io/">Textual</a>공식문서에서 확인이 가능합니다. </p>
]]></description>
        </item>
    </channel>
</rss>