<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>re_brother.log</title>
        <link>https://velog.io/</link>
        <description>@github https://github.com/jhpark-jarvis</description>
        <lastBuildDate>Wed, 29 Jul 2026 01:21:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>re_brother.log</title>
            <url>https://images.velog.io/images/re_brother/profile/07e8e4db-4942-4550-b944-3680fc068a59/제목 없음.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. re_brother.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/re_brother" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TIL] Docker로 실행한 FastAPI를 Oracle VM과 Cloudflare로 공개하기]]></title>
            <link>https://velog.io/@re_brother/TIL-Docker%EB%A1%9C-%EC%8B%A4%ED%96%89%ED%95%9C-FastAPI%EB%A5%BC-Oracle-VM%EA%B3%BC-Cloudflare%EB%A1%9C-%EA%B3%B5%EA%B0%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@re_brother/TIL-Docker%EB%A1%9C-%EC%8B%A4%ED%96%89%ED%95%9C-FastAPI%EB%A5%BC-Oracle-VM%EA%B3%BC-Cloudflare%EB%A1%9C-%EA%B3%B5%EA%B0%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 29 Jul 2026 01:21:21 GMT</pubDate>
            <description><![CDATA[<p>FastAPI 애플리케이션을 로컬에서 실행하는 것과 외부 사용자가 접속할 수 있는 서비스로 공개하는 것은 다른 문제였다.</p>
<p>이번 글에서는 Oracle Always Free VM에서 실행 중인 Docker 기반 FastAPI를 Nginx reverse proxy와 Cloudflare DNS/Proxy를 통해 도메인으로 연결한 과정을 정리한다.</p>
<p>애플리케이션 실행 환경은 Cloudflare Worker가 아니다. FastAPI는 표준 Docker 컨테이너로 실행하고, Cloudflare는 DNS, Proxy, D1, R2 역할을 담당한다.</p>
<h2 id="1-최종-아키텍처">1. 최종 아키텍처</h2>
<pre><code>사용자 브라우저
-&gt; example.com
    -&gt; Cloudflare DNS / Proxy
    -&gt; Oracle VM 공인 IP
    -&gt; Nginx :443
    -&gt; Nginx reverse proxy
    -&gt; 127.0.0.1:8000
    -&gt; Docker Compose
    -&gt; Uvicorn
    -&gt; FastAPI
    -&gt; Cloudflare D1 / R2</code></pre><p>각 계층의 책임은 다음처럼 나눴다.</p>
<ul>
<li>FastAPI: 애플리케이션과 API 제공</li>
<li>Docker: 실행 환경 패키징</li>
<li>Uvicorn: ASGI 애플리케이션 서버</li>
<li>Nginx: HTTPS 종료와 reverse proxy</li>
<li>Oracle VM: 애플리케이션 실행 서버</li>
<li>Cloudflare: DNS, Proxy, D1, R2</li>
<li>Gabia: 도메인 등록 기관</li>
</ul>
<p>FastAPI는 특정 클라우드의 실행 런타임에 종속되지 않는다. Oracle VM 대신 다른 Linux VM이나 컨테이너 호스팅 서비스를 사용하더라도 Docker 실행 계층은 재사용할 수 있다.</p>
<h2 id="2-oracle-vm에-docker-애플리케이션-실행">2. Oracle VM에 Docker 애플리케이션 실행</h2>
<p>Ubuntu VM에 Docker, Compose plugin, Git을 설치했다.</p>
<pre><code>sudo apt update
sudo apt install -y docker.io docker-compose-v2 git
sudo systemctl enable --now docker
sudo usermod -aG docker &quot;$USER&quot;</code></pre><p>그룹 권한을 적용하기 위해 SSH 세션을 종료한 뒤 다시 접속했다.</p>
<pre><code>exit</code></pre><p>소스는 배포 브랜치에서 내려받는다.</p>
<pre><code>mkdir -p ~/apps
cd ~/apps
git clone --branch feature/FastAPI-migration &lt;repository-url&gt;
cd &lt;repository-directory&gt;</code></pre><p>운영 환경변수는 저장소에 커밋하지 않고 VM에서 직접 작성한다.</p>
<pre><code>cp .env.example .env
chmod 600 .env
nano .env</code></pre><p>주요 설정은 다음과 같다.</p>
<pre><code>SECRET_KEY=&lt;long-random-secret&gt;
DATABASE=/app/instance/app.db
UPLOAD_FOLDER=/app/uploads
MAX_CONTENT_LENGTH=20971520
DISPLAY_TIMEZONE=Asia/Seoul

REPOSITORY_BACKEND=d1
STORAGE_BACKEND=r2

CLOUDFLARE_ACCOUNT_ID=&lt;cloudflare-account-id&gt;
D1_DATABASE_ID=&lt;d1-database-id&gt;
CLOUDFLARE_API_TOKEN=&lt;d1-runtime-api-token&gt;

R2_BUCKET_NAME=&lt;r2-bucket-name&gt;
R2_ACCOUNT_ID=&lt;cloudflare-account-id&gt;
R2_ACCESS_KEY_ID=&lt;r2-access-key-id&gt;
R2_SECRET_ACCESS_KEY=&lt;r2-secret-access-key&gt;
R2_PUBLIC_BASE_URL=&lt;r2-public-base-url&gt;</code></pre><p>D1 API Token과 R2 S3 Access Key는 서로 다른 인증 수단이다. CLOUDFLARE_API_TOKEN은 D1 REST API 호출에 사용하고, R2는 R2_ACCESS_KEY_ID와 R2_SECRET_ACCESS_KEY를 사용한다.</p>
<h2 id="3-docker-compose로-fastapi-실행">3. Docker Compose로 FastAPI 실행</h2>
<p>Oracle VM용 Compose 설정은 FastAPI 포트를 외부에 직접 노출하지 않도록 구성했다.</p>
<pre><code>services:
  fastapi:
    build:
      context: ../..
      dockerfile: Dockerfile
    container_name: personal-service-fastapi
    restart: unless-stopped
    env_file:
      - ../../.env
    ports:
      - &quot;127.0.0.1:8000:8000&quot;
    volumes:
      - fastapi-instance:/app/instance

volumes:
  fastapi-instance:</code></pre><p>컨테이너는 다음처럼 실행한다.</p>
<pre><code>cp deployment/oracle/docker-compose.yml.example deployment/oracle/docker-compose.yml
docker compose -f deployment/oracle/docker-compose.yml up -d --build</code></pre><p>FastAPI는 127.0.0.1:8000에서만 수신한다. 외부 요청은 Nginx만 받도록 하여 애플리케이션 포트를 인터넷에 직접 공개하지 않는다.</p>
<h2 id="4-nginx-reverse-proxy-구성">4. Nginx reverse proxy 구성</h2>
<p>Docker 컨테이너가 실행된 다음 VM에 Nginx를 설치한다.</p>
<pre><code>sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx</code></pre><p>/etc/nginx/sites-available/example.com에 다음 설정을 작성한다.</p>
<pre><code>server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    ssl_certificate /etc/nginx/ssl/example.com.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}</code></pre><p>사이트를 활성화한다.</p>
<pre><code>sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx</code></pre><p>Nginx는 외부의 80, 443 요청을 받아 내부 FastAPI의 8000 포트로 전달한다. FastAPI 포트를 127.0.0.1에만 바인딩했기 때문에 외부 사용자는 Nginx를 거치지 않고 컨테이너에 직접 접근할 수 없다.</p>
<h2 id="5-oracle-네트워크-포트-설정">5. Oracle 네트워크 포트 설정</h2>
<p>Oracle Cloud Console의 VCN 보안 목록에 다음 Ingress Rule을 추가했다.</p>
<pre><code>Source CIDR: 0.0.0.0/0
Protocol: TCP
Destination Port: 80

Source CIDR: 0.0.0.0/0
Protocol: TCP
Destination Port: 443</code></pre><p>SSH 접속을 위한 22번 포트도 필요하다. 운영 환경에서는 0.0.0.0/0 대신 본인의 고정 IP 대역으로 제한하는 것이 안전하다.</p>
<p>Oracle 보안 목록을 통과해도 Ubuntu 내부 방화벽 규칙에서 차단될 수 있다. 서버에서는 iptables의 INPUT 규칙에 80, 443을 허용하고 영구 저장했다.</p>
<pre><code>sudo iptables -I INPUT 5 -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
sudo iptables -I INPUT 5 -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
sudo apt install -y iptables-persistent
sudo netfilter-persistent save</code></pre><p>외부 연결은 다음 세 계층을 모두 통과해야 한다.</p>
<pre><code>Oracle VCN Security List -&gt; Ubuntu iptables -&gt; Nginx</code></pre><h2 id="6-gabia-도메인을-cloudflare로-연결">6. Gabia 도메인을 Cloudflare로 연결</h2>
<p>구매한 도메인은 Gabia에 그대로 두고 DNS 운영만 Cloudflare로 이전했다.</p>
<p>Cloudflare에서 도메인을 추가하면 사용할 네임서버를 할당한다. 이번에 할당된 네임서버는 다음과 같았다.</p>
<pre><code>&lt;cloudflare-nameserver-1&gt;
&lt;cloudflare-nameserver-2&gt;</code></pre><p>Gabia 도메인 관리 화면의 네임서버 설정에서 기존 Gabia 네임서버를 삭제하고 Cloudflare 네임서버를 등록한다.</p>
<pre><code>1차: &lt;cloudflare-nameserver-1&gt;
2차: &lt;cloudflare-nameserver-2&gt;</code></pre><p>DNSSEC를 사용 중이었다면 네임서버를 바꾸기 전에 비활성화해야 한다. DNSSEC 상태가 남아 있으면 네임서버 변경 후 DNS 응답 검증이 실패할 수 있다.</p>
<p>네임서버 전파 여부는 다음 명령으로 확인할 수 있다.</p>
<pre><code>nslookup -type=ns example.com 1.1.1.1</code></pre><h2 id="7-cloudflare-dns-레코드-설정">7. Cloudflare DNS 레코드 설정</h2>
<p>Cloudflare DNS에 Oracle VM의 Reserved Public IP를 등록한다.</p>
<pre><code>Type: A
Name: @
IPv4 address: &lt;oracle-reserved-public-ip&gt;
Proxy status: Proxied</code></pre><p>www도 사용할 경우 CNAME을 추가한다.</p>
<pre><code>Type: CNAME
Name: www
Target: example.com
Proxy status: Proxied</code></pre><p>Oracle VM의 공인 IP는 Ephemeral IP가 아닌 Reserved IP를 사용하는 편이 안전하다. Reserved IP는 해제하기 전까지 유지할 수 있어 VM 교체 시에도 같은 IP를 다시 연결할 수 있다.</p>
<h2 id="8-cloudflare-origin-certificate">8. Cloudflare Origin Certificate</h2>
<p>Cloudflare Proxy가 HTTPS로 원본 서버에 연결하려면 Oracle VM의 Nginx에도 인증서가 필요하다.</p>
<p>Cloudflare Dashboard에서 다음 메뉴로 이동한다.</p>
<pre><code>SSL/TLS
  -&gt; Origin Server
  -&gt; Create Certificate</code></pre><p>호스트 이름에는 다음을 포함한다.</p>
<pre><code>example.com
*.example.com</code></pre><p>인증서 발급 후 화면에 표시되는 두 값을 구분해야 한다.</p>
<pre><code>Origin Certificate -&gt; 공개 인증서 파일(.pem)
Private Key        -&gt; 개인 키 파일(.key)</code></pre><p>두 값을 각각 VM에 저장한다.</p>
<pre><code>sudo mkdir -p /etc/nginx/ssl
sudo nano /etc/nginx/ssl/example.com.pem
sudo nano /etc/nginx/ssl/example.com.key
sudo chmod 600 /etc/nginx/ssl/example.com.key</code></pre><p>Private Key는 저장소, 메신저, 블로그, 화면 캡처에 포함하면 안 된다.</p>
<h2 id="9-cloudflare-ssl-모드">9. Cloudflare SSL 모드</h2>
<p>원본 Nginx에도 Cloudflare Origin Certificate를 설치했으므로 Cloudflare의 SSL/TLS 암호화 모드는 Full (strict)를 사용한다.</p>
<pre><code>Full
  Cloudflare에서 원본까지 HTTPS로 연결하지만 인증서 검증은 엄격하지 않음

Full (strict)
  Cloudflare에서 원본까지 HTTPS로 연결하고 인증서 유효성 및 호스트 이름을 검증</code></pre><p>원본 인증서가 정상적으로 설치된 운영 환경에서는 Full (strict)가 적합하다. Cloudflare와 원본 사이에서도 인증서를 검증하므로 Full보다 안전하다.</p>
<h2 id="10-배포-구조-확인">10. 배포 구조 확인</h2>
<p>각 구간을 분리하면 연결 문제를 빠르게 찾을 수 있다.</p>
<pre><code>1. 컨테이너 -&gt; 127.0.0.1:8000
2. Nginx -&gt; 127.0.0.1:8000
3. VM 공인 IP -&gt; Nginx :80/:443
4. 도메인 DNS -&gt; Cloudflare
5. Cloudflare Proxy -&gt; Oracle Origin</code></pre><p>서버 내부에서 Nginx가 FastAPI로 전달하는지 확인할 때는 Host 헤더를 지정한다.</p>
<pre><code>curl -H &quot;Host: example.com&quot; http://127.0.0.1/health</code></pre><p>도메인과 HTTPS 연결 후에는 다음처럼 접근한다.</p>
<pre><code>curl.exe -i https://example.com/health</code></pre><p>Cloudflare Proxy를 사용하는 경우 외부 응답에는 Cloudflare 관련 헤더가 표시되고, 응답 본문은 FastAPI가 반환한 내용이어야 한다.</p>
<h2 id="11-배포-결과와-한계">11. 배포 결과와 한계</h2>
<p>최종적으로 애플리케이션 실행과 저장소를 다음처럼 분리했다.</p>
<pre><code>실행 계층: Oracle Always Free VM + Docker + Nginx
데이터 계층: Cloudflare D1
파일 계층: Cloudflare R2
네트워크 계층: Cloudflare DNS / Proxy</code></pre><p>FastAPI는 Worker 전용 코드 없이 일반 ASGI 애플리케이션으로 실행된다. 따라서 로컬에서는 Uvicorn으로 실행하고, 서버에서는 Docker Compose로 실행하며, 필요하면 다른 클라우드의 VM이나 컨테이너 플랫폼으로 옮길 수 있다.</p>
<p>Always Free VM은 리전의 호스트 용량과 계정 상태에 영향을 받고, E2.1.Micro처럼 자원이 작은 인스턴스는 이미지 빌드와 메모리 사용량에 주의해야 한다. 실행 서버를 교체할 수 있도록 데이터와 파일을 D1/R2에 분리한 이유도 여기에 있다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 배포의 핵심은 FastAPI를 Cloudflare Worker로 변환하지 않고, 일반 Docker 서비스로 실행하면서 Cloudflare의 관리형 서비스를 연결한 것이다.</p>
<pre><code>FastAPI는 어디서나 실행 가능하게
Cloudflare는 DNS, Proxy, D1, R2로 사용
Nginx는 외부 요청과 내부 애플리케이션 사이의 경계로 사용</code></pre><p>이 구조를 기반으로 이후에는 자동 배포, 운영 로그, 백업, 접근 제어와 모니터링을 추가할 수 있다.</p>
<h2 id="부록-pythonanywhere와-응답-속도-비교">부록. PythonAnywhere와 응답 속도 비교</h2>
<p>배포 후 기존 PythonAnywhere 주소와 새 도메인의 응답 속도를 간단히 비교했다.</p>
<p>두 서비스에서 공통으로 응답하는 루트 경로를 대상으로 PowerShell에서 각각 3회 요청했고, DNS 조회나 페이지 렌더링 시간은 제외하고 HTTP 요청의 전체 응답 시간을 측정했다.</p>
<pre><code class="language-text">측정 경로: /
측정 횟수: 각 3회

PythonAnywhere 평균: 약 0.893초
Oracle VM + Cloudflare 평균: 약 0.491초</code></pre>
<p>측정 환경에서는 새 배포 구조가 기존 PythonAnywhere보다 약 0.4초 빠르게 응답했으며, 전체 응답 시간이 약 45% 감소했다.</p>
<p>다만 이 결과는 단일 환경에서 짧게 측정한 참고값이다. 실제 속도는 네트워크 상태, Cloudflare Proxy 경로, 서버 부하, 응답 데이터 크기와 캐시 여부에 따라 달라질 수 있다. 또한 <code>/health</code> 경로는 PythonAnywhere 서비스에 존재하지 않아 <code>404</code>가 반환되었으므로 속도 비교에서는 사용하지 않았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] FastAPI 서비스를 Oracle Always Free VM에 배포하기(2)]]></title>
            <link>https://velog.io/@re_brother/TIL-FastAPI-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-Oracle-Always-Free-VM%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B02</link>
            <guid>https://velog.io/@re_brother/TIL-FastAPI-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-Oracle-Always-Free-VM%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B02</guid>
            <pubDate>Tue, 28 Jul 2026 11:44:12 GMT</pubDate>
            <description><![CDATA[<p>FastAPI 마이그레이션을 진행한 뒤에는 애플리케이션을 실제 서버에서 실행해봐야 했다.</p>
<p>처음에는 Cloudflare에서 FastAPI 애플리케이션까지 실행하는 구조를 검토했다. 하지만 Cloudflare Container는 Workers Paid 플랜이 필요했고, 애플리케이션 실행 환경을 Cloudflare에 종속시키지 않으려는 기존 방향과도 완전히 맞지는 않았다.</p>
<p>그래서 실행 서버는 Oracle Cloud의 Always Free VM으로 분리하고, 데이터와 파일 저장소는 기존처럼 Cloudflare D1/R2를 사용하는 구조를 선택했다.</p>
<h2 id="1-최종적으로-선택한-구조">1. 최종적으로 선택한 구조</h2>
<pre><code class="language-text">사용자 브라우저
    -&gt; Cloudflare DNS / Proxy
    -&gt; Oracle Always Free VM
    -&gt; Docker Compose
    -&gt; Uvicorn
    -&gt; FastAPI
    -&gt; Cloudflare D1 / R2</code></pre>
<p>각 서비스의 역할은 다음처럼 분리했다.</p>
<ul>
<li>Oracle VM: FastAPI 애플리케이션 실행</li>
<li>Docker: 실행 환경 패키징</li>
<li>Uvicorn: ASGI 서버</li>
<li>Cloudflare D1: 운영 데이터 저장</li>
<li>Cloudflare R2: 에셋과 문서 이미지 저장</li>
<li>Cloudflare DNS/Proxy: 도메인, HTTPS, 외부 접근 제어</li>
</ul>
<p>FastAPI는 표준 Docker 이미지로 실행하므로 다른 VM이나 다른 클라우드로 옮겨도 같은 방식으로 배포할 수 있다.</p>
<h2 id="2-cloudflare-container-대신-oracle-vm을-선택한-이유">2. Cloudflare Container 대신 Oracle VM을 선택한 이유</h2>
<p>Cloudflare Container는 무료 Workers 플랜에서 사용할 수 없고 Workers Paid 플랜이 필요하다.</p>
<p>무료 티어를 우선하는 상황에서 Oracle Always Free VM은 다음 장점이 있었다.</p>
<ul>
<li>FastAPI를 일반 Docker 애플리케이션으로 실행할 수 있음</li>
<li>Cloudflare Worker 전용 런타임에 종속되지 않음</li>
<li>기존 D1/R2 연결을 유지할 수 있음</li>
<li>나중에 다른 VM이나 클라우드로 옮기기 쉬움</li>
</ul>
<p>최종 구조는 Oracle을 애플리케이션 실행 서버로만 사용하고, D1/R2는 저장소 서비스로 유지하는 방식이다.</p>
<h2 id="3-vcn과-subnet-구성">3. VCN과 Subnet 구성</h2>
<p>VM 생성 화면에서 네트워크를 함께 만들 수도 있지만, Public Subnet이 정확히 선택되지 않으면 공인 IPv4를 할당할 수 없다. 그래서 VCN을 먼저 만들었다.</p>
<p>Oracle Console에서 다음 메뉴로 이동했다.</p>
<pre><code class="language-text">Networking
  -&gt; Virtual cloud networks
  -&gt; Start VCN Wizard
  -&gt; Create VCN with Internet Connectivity</code></pre>
<p>사용한 CIDR 구성은 다음과 같다.</p>
<pre><code class="language-text">VCN CIDR Block:             10.0.0.0/16
Public Subnet CIDR Block:   10.0.1.0/24
Private Subnet CIDR Block:  10.0.2.0/24</code></pre>
<p>처음에는 Private Subnet 기본값이 Public Subnet과 같은 <code>10.0.1.0/24</code>로 들어가 있어서 CIDR이 겹쳤다. 같은 VCN 안의 Subnet은 주소 범위가 겹치면 안 되므로 Private Subnet을 <code>10.0.2.0/24</code>로 변경했다.</p>
<p>최종 네트워크는 다음과 같다.</p>
<pre><code class="language-text">fastapi-vcn
  -&gt; Public Subnet   10.0.1.0/24
  -&gt; Private Subnet  10.0.2.0/24</code></pre>
<p>VM은 외부 SSH와 웹 요청을 받아야 하므로 Public Subnet에 배치한다.</p>
<h2 id="4-a1-인스턴스-생성-시도와-용량-부족">4. A1 인스턴스 생성 시도와 용량 부족</h2>
<p>처음에는 Oracle Always Free의 Ampere A1 인스턴스를 사용하려고 했다.</p>
<pre><code class="language-text">Shape: VM.Standard.A1.Flex
OCPU: 1
Memory: 6GB
Image: Ubuntu</code></pre>
<p>그러나 다음 오류가 발생했다.</p>
<pre><code class="language-text">Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1.</code></pre>
<p>이 오류는 설정 오류나 결제 오류가 아니라 해당 Availability Domain의 호스트 용량 부족이다. 현재 계정의 Home Region에서는 AD-1만 표시되어 다른 Availability Domain으로 옮길 수도 없었다.</p>
<p>A1 용량이 언제 확보될지 알 수 없는 상태에서 기다리는 것보다 우선 서비스를 실행하기 위해 <code>VM.Standard.E2.1.Micro</code>를 생성했다.</p>
<pre><code class="language-text">Shape: VM.Standard.E2.1.Micro
Memory: 1GB</code></pre>
<p>E2.1.Micro는 테스트와 소규모 실행용으로는 사용할 수 있지만 메모리와 CPU가 작다. 또한 E2.1.Micro는 나중에 A1.Flex로 단순 변경할 수 없다.</p>
<p>A1 용량이 확보되면 새 VM을 만들고 같은 Docker 이미지와 환경변수로 재배포해야 한다. D1/R2를 외부 저장소로 사용하고 있기 때문에 실행 VM을 교체해도 핵심 데이터는 유지된다.</p>
<h2 id="5-public-ip와-네트워크-확인">5. Public IP와 네트워크 확인</h2>
<p>VM 생성 시 다음을 선택했다.</p>
<pre><code class="language-text">Virtual cloud network: fastapi-vcn
Subnet: Public Subnet
Public IPv4 address: Automatically assign</code></pre>
<p>VM 상세의 <code>Detail</code> 탭에는 IP 정보가 요약되어 보이지 않을 수 있다. 실제 IP는 다음 위치에서 확인할 수 있었다.</p>
<pre><code class="language-text">VM 상세 화면
  -&gt; Networking 탭
  -&gt; Primary VNIC
  -&gt; Private IPs</code></pre>
<p>공인 IP가 할당된 상태라면 Primary VNIC에 Private IP도 존재한다.</p>
<p>외부 접속에 필요한 보안 규칙도 추가했다.</p>
<pre><code class="language-text">TCP 22  SSH
TCP 80  HTTP
TCP 443 HTTPS</code></pre>
<h2 id="6-windows에서-ssh-연결">6. Windows에서 SSH 연결</h2>
<p>Oracle에서 SSH Private Key를 다운로드한 뒤 Windows에서 접속하려고 하자 다음 오류가 발생했다.</p>
<pre><code class="language-text">WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions for &#39;ssh-key.key&#39; are too open.</code></pre>
<p>Windows 파일 권한에 <code>Authenticated Users</code>가 포함되어 있어서 OpenSSH가 개인 키를 거부한 것이다.</p>
<p>PowerShell에서 상속 권한을 제거하고 현재 사용자에게만 읽기 권한을 부여했다.</p>
<pre><code class="language-powershell">$key = &quot;G:\Oracle_key\ssh-key.key&quot;
icacls $key /inheritance:r
icacls $key /remove &quot;Authenticated Users&quot; &quot;Users&quot; &quot;Everyone&quot;
icacls $key /grant:r &quot;$($env:USERNAME):(R)&quot;</code></pre>
<p>그 후 다음 명령으로 접속할 수 있었다.</p>
<pre><code class="language-powershell">ssh -i &quot;G:\Oracle_key\ssh-key.key&quot; ubuntu@&lt;oracle-public-ip&gt;</code></pre>
<p>Windows에서는 별도의 SSH 프로그램보다 Windows Terminal의 기본 OpenSSH 클라이언트를 사용하는 편이 간단했다. 이후 원격 파일 편집이 필요하면 VS Code Remote-SSH를 사용할 수 있다.</p>
<h2 id="7-e21micro에-swap-추가">7. E2.1.Micro에 Swap 추가</h2>
<p>E2.1.Micro는 메모리가 1GB이므로 Docker 설치나 이미지 빌드 중 메모리 부족이 발생할 수 있다. 그래서 2GB Swap을 추가했다.</p>
<pre><code class="language-bash">sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo &#39;/swapfile none swap sw 0 0&#39; | sudo tee -a /etc/fstab
free -h</code></pre>
<p>Swap은 RAM을 대체하지 않지만 설치 과정에서 프로세스가 즉시 종료되는 상황을 완화할 수 있다.</p>
<h2 id="8-docker와-fastapi-소스-설치">8. Docker와 FastAPI 소스 설치</h2>
<p>Ubuntu VM에 Docker, Compose plugin, Git을 설치했다.</p>
<pre><code class="language-bash">sudo apt update
sudo apt install -y docker.io docker-compose-v2 git
sudo systemctl enable --now docker
sudo usermod -aG docker &quot;$USER&quot;</code></pre>
<p>Docker 그룹 권한을 적용하기 위해 SSH 세션을 종료한 뒤 다시 접속했다.</p>
<pre><code class="language-bash">exit</code></pre>
<p>이후 마이그레이션 브랜치의 소스를 내려받았다.</p>
<pre><code class="language-bash">mkdir -p ~/apps
cd ~/apps
git clone --branch feature/FastAPI-migration https://github.com/jhpark-jarvis/maple-life-docs.git
cd maple-life-docs</code></pre>
<h2 id="9-운영-환경변수-설정">9. 운영 환경변수 설정</h2>
<p>운영 환경변수는 저장소에 커밋하지 않고 서버에서 직접 작성한다.</p>
<pre><code class="language-bash">cp .env.example .env
chmod 600 .env
nano .env</code></pre>
<p>주요 설정은 다음과 같다.</p>
<pre><code class="language-env">SECRET_KEY=&lt;long-random-secret&gt;
DATABASE=/app/instance/app.db
UPLOAD_FOLDER=/app/uploads
MAX_CONTENT_LENGTH=20971520
DISPLAY_TIMEZONE=Asia/Seoul

REPOSITORY_BACKEND=d1
STORAGE_BACKEND=r2

CLOUDFLARE_ACCOUNT_ID=&lt;cloudflare-account-id&gt;
D1_DATABASE_ID=&lt;d1-database-id&gt;
CLOUDFLARE_API_TOKEN=&lt;d1-runtime-api-token&gt;

R2_BUCKET_NAME=&lt;r2-bucket-name&gt;
R2_ACCOUNT_ID=&lt;cloudflare-account-id&gt;
R2_ACCESS_KEY_ID=&lt;r2-access-key-id&gt;
R2_SECRET_ACCESS_KEY=&lt;r2-secret-access-key&gt;
R2_PUBLIC_BASE_URL=&lt;r2-public-base-url&gt;

FLASK_ENV=production
FLASK_DEBUG=0</code></pre>
<p>처음 <code>.env.example</code>에는 다음과 같은 placeholder가 있었다.</p>
<pre><code class="language-env">DATABASE={}
UPLOAD_FOLDER={}</code></pre>
<p>이 값은 실제 경로가 아니므로 컨테이너에서는 중괄호를 제거해야 한다. 잘못 입력하면 <code>{/app/uploads}</code> 전체가 경로로 해석되어 FastAPI가 시작되지 않는다.</p>
<h2 id="10-cloudflare-d1-api-token">10. Cloudflare D1 API Token</h2>
<p>로컬 <code>.env</code>에는 <code>CLOUDFLARE_API_TOKEN</code>이 없었지만 로컬 D1 연결은 정상 동작했다. 로컬에서는 Wrangler 로그인 정보가 fallback으로 사용되고 있었기 때문이다.</p>
<p>하지만 Oracle VM에는 로컬 PC의 Wrangler 인증 파일이 없으므로 D1 REST API를 호출하려면 별도의 API Token이 필요하다.</p>
<p>Cloudflare에서 다음 권한으로 런타임 토큰을 만들었다.</p>
<pre><code class="language-text">Token name: oracle-fastapi-d1-runtime

Account
  -&gt; D1
      -&gt; Edit</code></pre>
<p>Account Resources는 사용하는 Cloudflare Account 하나로 제한했다. Zone, DNS, Worker, R2 관리 권한은 추가하지 않았다.</p>
<p>R2는 Cloudflare API Token이 아니라 S3 호환 API용 Access Key와 Secret Access Key로 접근한다.</p>
<pre><code class="language-env">R2_ACCESS_KEY_ID=&lt;r2-access-key-id&gt;
R2_SECRET_ACCESS_KEY=&lt;r2-secret-access-key&gt;</code></pre>
<p>따라서 D1 API Token과 R2 S3 인증키는 서로 다른 용도다.</p>
<h2 id="11-oracle-docker-compose-구성">11. Oracle Docker Compose 구성</h2>
<p>Oracle 배포용 Compose 설정은 다음과 같다.</p>
<pre><code class="language-yaml">services:
  fastapi:
    build:
      context: ../..
      dockerfile: Dockerfile
    container_name: personal-service-fastapi
    restart: unless-stopped
    env_file:
      - ../../.env
    ports:
      - &quot;127.0.0.1:8000:8000&quot;
    volumes:
      - fastapi-instance:/app/instance

volumes:
  fastapi-instance:</code></pre>
<p>외부 포트를 직접 공개하지 않고 <code>127.0.0.1:8000</code>에만 바인딩했다. 이후 Nginx가 80/443 요청을 받고 내부 FastAPI로 전달하게 된다.</p>
<p><code>/app/instance</code>는 named volume으로 보존한다. D1이 운영 데이터의 원본이지만, SQLite shadow DB와 페이지뷰 로그가 컨테이너 재생성 때 사라지지 않도록 하기 위해서다.</p>
<p>컨테이너는 다음 명령으로 실행했다.</p>
<pre><code class="language-bash">cp deployment/oracle/docker-compose.yml.example deployment/oracle/docker-compose.yml
docker compose -f deployment/oracle/docker-compose.yml up -d --build</code></pre>
<h2 id="12-첫-실행-오류와-설정-로더-수정">12. 첫 실행 오류와 설정 로더 수정</h2>
<p>처음 컨테이너를 실행했을 때 다음 오류가 발생했다.</p>
<pre><code class="language-text">RuntimeError: Directory &#39;{/app/uploads}&#39; does not exist</code></pre>
<p>원인은 <code>.env</code>에 경로를 다음처럼 입력했기 때문이다.</p>
<pre><code class="language-env">UPLOAD_FOLDER={/app/uploads}</code></pre>
<p>또한 기존 설정 로더는 기본 <code>uploads/</code> 디렉터리는 만들고 있었지만 환경변수로 지정된 <code>UPLOAD_FOLDER</code>는 생성하지 않고 있었다.</p>
<p>이 문제를 해결하기 위해 설정 로더를 수정했다.</p>
<pre><code class="language-python">database = os.environ.get(&quot;DATABASE&quot;, str(instance_path / &quot;app.db&quot;))
upload_folder = os.environ.get(&quot;UPLOAD_FOLDER&quot;, str(default_upload_dir))

Path(database).parent.mkdir(parents=True, exist_ok=True)
Path(upload_folder).mkdir(parents=True, exist_ok=True)</code></pre>
<p>서버의 <code>.env</code>도 다음처럼 수정했다.</p>
<pre><code class="language-bash">sed -i &#39;s|^DATABASE=.*|DATABASE=/app/instance/app.db|&#39; .env
sed -i &#39;s|^UPLOAD_FOLDER=.*|UPLOAD_FOLDER=/app/uploads|&#39; .env</code></pre>
<p>이후 최신 코드를 반영하고 컨테이너를 재생성했다.</p>
<pre><code class="language-bash">git pull origin feature/FastAPI-migration
docker compose -f deployment/oracle/docker-compose.yml up -d --build --force-recreate</code></pre>
<h2 id="13-fastapi와-d1-연결-확인">13. FastAPI와 D1 연결 확인</h2>
<p>VM 내부에서 health endpoint를 호출했다.</p>
<pre><code class="language-bash">curl http://127.0.0.1:8000/health</code></pre>
<p>응답은 다음과 같았다.</p>
<pre><code class="language-json">{
  &quot;ok&quot;: true,
  &quot;service&quot;: &quot;personal-service-fastapi&quot;,
  &quot;repository_backend&quot;: &quot;d1&quot;,
  &quot;storage_backend&quot;: &quot;r2&quot;,
  &quot;database_configured&quot;: true,
  &quot;database_role&quot;: &quot;shadow&quot;
}</code></pre>
<p><code>database_role</code>이 <code>shadow</code>인 것은 오류가 아니다.</p>
<pre><code class="language-text">D1 = 운영 데이터 원본
SQLite = 공용 repository helper가 사용하는 로컬 shadow DB</code></pre>
<p>D1 API가 실제로 동작하는지 문서 목록 API도 호출했다.</p>
<pre><code class="language-bash">curl &quot;http://127.0.0.1:8000/api/documents?per_page=1&quot;</code></pre>
<p>이 단계에서 다음 항목을 확인했다.</p>
<ul>
<li>Docker 컨테이너 실행</li>
<li>Uvicorn 프로세스 시작</li>
<li>FastAPI ASGI 엔트리포인트 로딩</li>
<li>환경변수 기반 설정 로딩</li>
<li>로컬 shadow SQLite 생성</li>
<li>Cloudflare D1 API 연결</li>
<li>FastAPI health endpoint 응답</li>
</ul>
<h2 id="14-현재-남은-작업">14. 현재 남은 작업</h2>
<p>현재 컨테이너는 <code>127.0.0.1:8000</code>에만 열려 있어 외부 브라우저에서 직접 접근할 수 없다.</p>
<p>다음 순서로 운영 연결을 진행한다.</p>
<ol>
<li>Oracle VM에 Nginx 설치</li>
<li>Nginx에서 <code>80/443 -&gt; 127.0.0.1:8000</code> reverse proxy 설정</li>
<li>가비아에서 구매한 도메인을 Cloudflare에 추가</li>
<li>가비아 네임서버를 Cloudflare 네임서버로 변경</li>
<li>Cloudflare DNS에 Oracle VM 공인 IP를 A 레코드로 등록</li>
<li>Cloudflare Origin Certificate 또는 공개 인증서 설정</li>
<li>Cloudflare SSL/TLS를 <code>Full (strict)</code>로 설정</li>
<li>Cloudflare Access 또는 별도 인증으로 서비스 보호</li>
</ol>
<p>현재 FastAPI API에는 별도 로그인 인증이 없으므로 도메인을 바로 공개하기 전에 접근 제어를 추가해야 한다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 작업을 통해 FastAPI 애플리케이션을 Oracle VM에서 Docker로 실행하고, Cloudflare D1 연결까지 확인했다.</p>
<p>A1 인스턴스 용량 부족으로 E2.1.Micro를 임시 실행 서버로 선택했지만, 애플리케이션은 Docker와 외부 저장소에 분리되어 있다. 나중에 A1 VM을 새로 만들더라도 같은 이미지와 환경변수로 재배포할 수 있다.</p>
<p>이번 단계에서 확인한 핵심은 다음과 같다.</p>
<ul>
<li>FastAPI는 Cloudflare Worker 없이 일반 Docker 서버에서 실행 가능하다.</li>
<li>Oracle Always Free VM을 애플리케이션 실행 서버로 사용할 수 있다.</li>
<li>로컬 Wrangler 인증과 서버용 D1 API Token은 구분해야 한다.</li>
<li>R2 업로드에는 Cloudflare API Token이 아니라 R2 S3 Access Key가 필요하다.</li>
<li>컨테이너 환경에서는 환경변수 경로를 실제 디렉터리로 지정하고 생성해야 한다.</li>
<li>실행 서버를 교체해도 D1/R2를 유지하면 애플리케이션 재배포가 단순해진다.</li>
</ul>
<p>다음 글에서는 Nginx reverse proxy와 가비아 도메인, Cloudflare DNS, HTTPS 연결 과정을 정리할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] FastAPI 서비스를 Oracle Always Free VM에 올리기(1)]]></title>
            <link>https://velog.io/@re_brother/TIL-FastAPI-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-Oracle-Always-Free-VM%EC%97%90-%EC%98%AC%EB%A6%AC%EA%B8%B01</link>
            <guid>https://velog.io/@re_brother/TIL-FastAPI-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-Oracle-Always-Free-VM%EC%97%90-%EC%98%AC%EB%A6%AC%EA%B8%B01</guid>
            <pubDate>Tue, 28 Jul 2026 04:51:43 GMT</pubDate>
            <description><![CDATA[<p>FastAPI 마이그레이션을 진행하면서 애플리케이션을 어디에서 실행할지 결정해야 했다.</p>
<p>처음에는 <strong>Cloudflare</strong>에서 FastAPI까지 실행하는 방법을 검토했다. <strong>Cloudflare Container</strong>를 사용하면 Docker 기반 FastAPI를 실행할 수 있지만, 현재 기준으로 Container는 Workers Paid 플랜이 필요하다. 무료 티어를 유지하면서 FastAPI를 실행하려면 애플리케이션 실행 서버와 Cloudflare의 저장소 서비스를 분리하는 편이 더 적합했다.</p>
<p>이번 글에서는 Oracle Cloud의 Always Free VM을 생성하고, 이후 Docker 기반 FastAPI 서버를 올릴 수 있는 네트워크 환경을 구성하는 과정을 정리한다.</p>
<h2 id="1-선택한-운영-구조">1. 선택한 운영 구조</h2>
<p>이번에 선택한 구조는 다음과 같다.</p>
<pre><code class="language-text">사용자 브라우저
    -&gt; Cloudflare DNS / Proxy
    -&gt; Oracle Always Free VM
    -&gt; Docker
    -&gt; Uvicorn
    -&gt; FastAPI
    -&gt; Cloudflare D1 / R2</code></pre>
<p>FastAPI 애플리케이션은 특정 Cloudflare 런타임에 맞춰 작성하지 않는다. 일반적인 Docker 컨테이너로 실행하기 때문에 로컬, Oracle Cloud, 다른 VM, 다른 클라우드 환경에서도 같은 이미지를 사용할 수 있다.</p>
<p>Cloudflare는 계속 다음 역할을 담당한다.</p>
<blockquote>
<ul>
<li>D1: 운영 데이터베이스</li>
</ul>
</blockquote>
<ul>
<li>R2: 파일 저장소</li>
<li>DNS 및 필요한 경우 Proxy</li>
</ul>
<p>즉, 애플리케이션 실행 환경과 저장소 서비스를 분리한다. 이 구조를 선택하면 무료 실행 환경을 사용할 수 있고, 나중에 서버를 옮길 때 FastAPI 코드와 Dockerfile을 그대로 재사용할 수 있다.</p>
<p>Oracle 공식 문서 기준 Always Free 리소스에는 Ampere A1 Compute가 포함되어 있다. 전체 한도는 월 기준 <code>2 OCPU</code>, <code>12GB 메모리</code> 수준이며, 계정과 리전에 따라 실제 생성 가능 여부는 달라질 수 있다.</p>
<p>참고: <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm">Oracle Always Free 리소스</a></p>
<h2 id="2-리전-확인">2. 리전 확인</h2>
<p>Oracle Cloud Console에 로그인한 뒤 우측 상단의 리전을 확인한다.</p>
<p>Always Free VM은 기본적으로 계정의 Home Region에서 생성하는 것이 안전하다. 특정 리전에서 Always Free 인스턴스의 호스트 용량이 부족하면 생성 시 <code>Out of host capacity</code> 오류가 발생할 수 있다. 이 경우 같은 리전의 다른 Availability Domain을 선택하거나 잠시 후 다시 시도한다.</p>
<h2 id="3-vcn과-public-subnet-만들기">3. VCN과 Public Subnet 만들기</h2>
<p>VM 생성 화면에서 네트워크를 한 번에 만들 수도 있지만, Public Subnet이 제대로 선택되지 않으면 공인 IPv4를 할당할 수 없다. 그래서 먼저 VCN을 별도로 생성한다.</p>
<ol>
<li>좌측 상단 메뉴를 연다.</li>
<li><code>Networking</code>을 선택한다.</li>
<li><code>Virtual cloud networks</code>로 이동한다.</li>
<li><code>Start VCN Wizard</code>를 클릭한다.</li>
<li><code>Create VCN with Internet Connectivity</code>를 선택한다.</li>
<li><code>Start VCN Wizard</code>를 클릭한다.</li>
</ol>
<p>다음 값을 입력한다.</p>
<pre><code class="language-text">VCN Name: fastapi-vcn
IPv4 CIDR Block: 10.0.0.0/16
Public Subnet CIDR Block: 10.0.1.0/24
Private Subnet CIDR Block: 기본값</code></pre>
<p>VCN 이름은 원하는 이름을 사용해도 된다. CIDR 값은 새 VCN을 만드는 경우 사용할 수 있는 예시값이다.</p>
<p><code>Create</code>를 클릭하면 Oracle이 다음 네트워크 구성요소를 함께 만든다.</p>
<ul>
<li>Virtual Cloud Network</li>
<li>Internet Gateway</li>
<li>Route Table</li>
<li>Public Subnet</li>
<li>Private Subnet</li>
</ul>
<p>생성이 끝나면 VCN 상세 화면의 <code>Subnets</code>에서 <code>Public Subnet</code>이 생성되었는지 확인한다. 이후 VM에는 이 Public Subnet을 선택해야 한다.</p>
<p>Oracle에서는 Private Subnet의 리소스에 공인 IPv4 주소를 할당할 수 없다. 공인 IP를 사용하려면 반드시 Public Subnet을 선택해야 한다.</p>
<p>참고: <a href="https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/launchinginstance.htm">Oracle 인스턴스 생성 문서</a></p>
<h2 id="4-compute-instance-생성">4. Compute Instance 생성</h2>
<ol>
<li>좌측 상단 메뉴를 연다.</li>
<li><code>Compute</code>를 선택한다.</li>
<li><code>Instances</code>로 이동한다.</li>
<li><code>Create instance</code>를 클릭한다.</li>
</ol>
<h3 id="41-기본-정보">4.1 기본 정보</h3>
<pre><code class="language-text">Name: fastapi-server
Compartment: 기본 Root compartment
Availability Domain: 기본값
Capacity Type: On-demand capacity
Fault Domain: 기본값</code></pre>
<p>Availability Domain은 <code>Always Free Eligible</code>로 표시되는 항목을 우선 선택한다. 화면에 표시되는 항목은 리전마다 다를 수 있으므로 기본 선택값이 Always Free 대상인지 확인한다.</p>
<h3 id="42-image-and-shape">4.2 Image and shape</h3>
<p><code>Image and shape</code> 영역에서 <code>Edit</code>를 클릭한다.</p>
<p>Image는 다음 중 하나를 선택한다.</p>
<pre><code class="language-text">Ubuntu 24.04
또는
Ubuntu 22.04</code></pre>
<p>그다음 <code>Change shape</code>를 클릭하고 다음처럼 설정한다.</p>
<pre><code class="language-text">Shape type: Virtual machine
Shape series: Ampere
Shape: VM.Standard.A1.Flex
OCPU: 1
Memory: 6 GB</code></pre>
<p><code>VM.Standard.A1.Flex</code>가 <code>Always Free-eligible</code>로 표시되는지 확인한 뒤 <code>Select shape</code>를 클릭한다.</p>
<p>이 서비스는 Docker 기반으로 실행할 예정이므로 ARM 기반인 Ampere 인스턴스를 사용해도 된다. 현재 Dockerfile의 Python 기본 이미지는 ARM 환경에서 사용할 수 있는 이미지를 제공한다.</p>
<h3 id="43-primary-vnic-및-네트워크">4.3 Primary VNIC 및 네트워크</h3>
<p><code>Primary VNIC</code> 또는 <code>Networking</code> 영역에서 다음과 같이 선택한다.</p>
<pre><code class="language-text">Virtual cloud network: Select existing virtual cloud network
VCN: fastapi-vcn
Subnet: Select existing subnet
Subnet: Public Subnet
Private IPv4 address: Automatically assign private IPv4 address
Public IPv4 address assignment: Automatically assign public IPv4 address</code></pre>
<p>여기서 가장 중요한 값은 <code>Subnet</code>이다. 반드시 앞에서 만든 <code>Public Subnet</code>을 선택해야 한다.</p>
<p>다음과 같은 경고가 나오면 현재 Subnet이 Public Subnet으로 선택되지 않은 상태다.</p>
<pre><code class="language-text">You must select a public subnet to assign a public IPv4 address.</code></pre>
<p>이 경우 <code>Private Subnet</code>을 선택했는지 확인하고, <code>Select existing subnet</code>에서 Public Subnet을 다시 선택한다.</p>
<p><code>Automatically assign public IPv4 address</code>는 인스턴스 생성 시 외부에서 접속 가능한 공인 IPv4 주소를 자동으로 할당하는 옵션이다. SSH 접속과 웹 서비스 공개를 위해 체크한다.</p>
<h3 id="44-ssh-키">4.4 SSH 키</h3>
<p><code>Add SSH keys</code> 영역에서 다음을 진행한다.</p>
<ol>
<li><code>Generate a key pair for me</code>를 선택한다.</li>
<li><code>Save Private Key</code>를 클릭한다.</li>
<li>다운로드된 <code>.key</code> 파일을 안전한 위치에 보관한다.</li>
<li>필요하다면 <code>Save Public Key</code>도 함께 저장한다.</li>
</ol>
<p>Private Key는 분실하면 안 된다. 공개 저장소에 올리거나 다른 사람에게 전달해서도 안 된다.</p>
<h3 id="45-boot-volume">4.5 Boot Volume</h3>
<p><code>Boot volume</code> 영역은 우선 기본값을 사용한다.</p>
<p>별도 디스크 구성이나 사용자 지정 부트 볼륨은 초기 배포에 필요하지 않다. 나중에 로그, 업로드 파일, 백업 정책을 정리할 때 별도로 검토한다.</p>
<h2 id="5-인스턴스-생성-및-확인">5. 인스턴스 생성 및 확인</h2>
<p>모든 설정을 확인한 뒤 하단의 <code>Create</code>를 클릭한다.</p>
<p>생성이 끝나면 인스턴스 상세 화면에서 다음 항목을 확인한다.</p>
<pre><code class="language-text">State: Running
Public IPv4 address: 숫자 형태의 공인 IP
Private IPv4 address: 숫자 형태의 사설 IP</code></pre>
<p><code>Public IPv4 address</code>가 비어 있다면 다음을 다시 확인한다.</p>
<ul>
<li>Subnet이 Public Subnet인지</li>
<li><code>Automatically assign public IPv4 address</code>가 체크되어 있는지</li>
<li>VCN에 Internet Gateway와 Route Table이 생성되어 있는지</li>
</ul>
<h2 id="6-80-443-포트-열기">6. 80, 443 포트 열기</h2>
<p>VCN의 보안 목록에서 웹 접속 포트를 허용한다.</p>
<ol>
<li>VM 상세 화면에서 <code>Primary VNIC</code>를 클릭한다.</li>
<li>연결된 <code>Subnet</code>을 클릭한다.</li>
<li><code>Default Security List</code>를 클릭한다.</li>
<li><code>Add Ingress Rules</code>를 클릭한다.</li>
</ol>
<p>HTTP 규칙을 추가한다.</p>
<pre><code class="language-text">Source CIDR: 0.0.0.0/0
IP Protocol: TCP
Destination Port Range: 80</code></pre>
<p>HTTPS 규칙도 추가한다.</p>
<pre><code class="language-text">Source CIDR: 0.0.0.0/0
IP Protocol: TCP
Destination Port Range: 443</code></pre>
<p>SSH 접속용 <code>22</code>번 포트가 이미 존재하는지도 확인한다. 없다면 다음 규칙을 추가한다.</p>
<pre><code class="language-text">Source CIDR: 0.0.0.0/0
IP Protocol: TCP
Destination Port Range: 22</code></pre>
<p>운영 환경에서는 SSH를 <code>0.0.0.0/0</code>으로 열어두기보다 본인의 고정 IP로 제한하는 것이 안전하다. 초기 접속 확인 후 보안 규칙을 다시 좁히는 것이 좋다.</p>
<h2 id="7-다음-작업">7. 다음 작업</h2>
<p>VM 생성이 끝났다고 FastAPI가 바로 실행되는 것은 아니다. 다음 순서로 서버를 구성해야 한다.</p>
<ol>
<li>SSH로 Oracle VM 접속</li>
<li>Docker 설치</li>
<li>저장소를 VM으로 가져오기</li>
<li>운영용 환경변수 설정</li>
<li>Docker 이미지 빌드</li>
<li>FastAPI 컨테이너 실행</li>
<li>Nginx 또는 Caddy로 80/443 연결</li>
<li>Cloudflare DNS에 도메인 연결</li>
<li><code>/health</code> 엔드포인트로 운영 상태 확인</li>
</ol>
<p>현재 애플리케이션은 <code>Dockerfile</code>에서 <code>uvicorn asgi:app</code>으로 FastAPI를 실행하도록 구성되어 있다. 따라서 Oracle VM에 배포할 때도 Cloudflare Worker 전용 코드가 아니라 일반 Docker 실행 절차를 사용한다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 단계에서는 애플리케이션을 실행할 무료 인프라를 마련했다. Cloudflare Container는 무료 티어에서 사용할 수 없었지만, Oracle Always Free VM을 사용하면 FastAPI를 특정 클라우드 런타임에 종속시키지 않고 Docker 애플리케이션으로 운영할 수 있다.</p>
<p>이 구조에서는 실행 서버를 다른 VM이나 클라우드로 옮겨도 FastAPI 코드와 Dockerfile을 그대로 재사용할 수 있다. Cloudflare D1과 R2는 저장소 계층으로만 사용하기 때문에 애플리케이션 실행 환경과의 결합도 줄어든다.</p>
<p>다음 글에서는 생성한 Oracle VM에 SSH로 접속하고, Docker를 설치한 뒤 FastAPI 컨테이너를 실행하는 과정을 정리할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Flask 프로젝트를 FastAPI로 이관하기(3)]]></title>
            <link>https://velog.io/@re_brother/TIL-Flask-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-FastAPI%EB%A1%9C-%EC%9D%B4%EA%B4%80%ED%95%98%EA%B8%B03</link>
            <guid>https://velog.io/@re_brother/TIL-Flask-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-FastAPI%EB%A1%9C-%EC%9D%B4%EA%B4%80%ED%95%98%EA%B8%B03</guid>
            <pubDate>Mon, 27 Jul 2026 07:14:45 GMT</pubDate>
            <description><![CDATA[<h2 id="실제-fastapi-이관-작업">실제 FastAPI 이관 작업</h2>
<h3 id="1-실제-마이그레이션-시작">1. 실제 마이그레이션 시작</h3>
<p>2편까지는 FastAPI로 전환하려는 이유와 작업 순서를 정리했다. 이번 글부터는 실제 코드에서 어떤 부분을 변경했는지 정리해보려고 한다.</p>
<p>이번 작업에서는 Flask 앱을 바로 삭제하지 않고, 기존 Flask 앱은 실행 가능한 상태로 유지하면서 별도의 FastAPI 앱을 추가했다. 이렇게 하면 기존 기능을 참고하면서 API를 하나씩 옮길 수 있고, 문제가 생겼을 때 비교할 수 있는 기준도 남는다.</p>
<h3 id="2-fastapi-앱-엔트리포인트-추가">2. FastAPI 앱 엔트리포인트 추가</h3>
<p>FastAPI 앱은 <code>fastapi_app/main.py</code>에서 생성하고, 외부 ASGI 서버가 사용할 수 있도록 <code>asgi.py</code>에서 앱을 노출했다.</p>
<pre><code class="language-python">from fastapi_app.main import app</code></pre>
<p>로컬에서는 다음과 같이 실행할 수 있다.</p>
<pre><code class="language-bash">uvicorn asgi:app --reload</code></pre>
<p>기존 Flask의 <code>run.py</code>는 바로 삭제하지 않고 호환 실행 경로로 남겨두었다.</p>
<h3 id="3-설정-로더-분리">3. 설정 로더 분리</h3>
<p>기존 Flask 앱은 <code>app.config</code>을 통해 환경변수와 설정값을 전달받았다. FastAPI에서도 같은 설정을 사용해야 하므로 환경변수 로딩 결과를 <code>AppSettings</code> 객체로 분리했다.</p>
<p>Flask는 이 객체를 <code>app.config</code>으로 변환하고, FastAPI는 <code>app.state.settings</code>에 보관하도록 했다. 이 방식으로 설정 로딩과 프레임워크별 앱 초기화를 나눌 수 있었다.</p>
<h3 id="4-repository-provider-이관">4. Repository provider 이관</h3>
<p>기존 프로젝트에는 SQLite와 D1을 분기하는 repository/provider 구조가 이미 있었다. 이 구조를 활용해 FastAPI에서는 요청의 dependency로 repository provider를 받도록 구성했다.</p>
<pre><code class="language-text">FastAPI Request
  -&gt; dependency
  -&gt; repository provider
      -&gt; SQLite repository
      -&gt; D1 repository</code></pre>
<p>Flask에서만 사용할 수 있었던 provider 생성 로직은 공용 runtime provider로 옮겼다. 이후 Flask 호환 경로도 같은 builder를 사용하도록 변경해 두 런타임의 저장소 선택 로직을 통합했다.</p>
<h3 id="5-주요-api-이관">5. 주요 API 이관</h3>
<p>기능별로 FastAPI router를 분리했다.</p>
<ul>
<li>대시보드</li>
<li>문서</li>
<li>에셋</li>
<li>WBS</li>
<li>일정</li>
<li>멤버</li>
<li>페이지뷰 로그</li>
</ul>
<p>문서와 에셋 API는 목록·상세·생성·수정·삭제뿐만 아니라 폴더, 태그, 일괄 처리, 업로드·다운로드까지 함께 이관해야 했다.</p>
<h3 id="6-flask-app-context-의존성-줄이기">6. Flask app context 의존성 줄이기</h3>
<p>실제 작업에서 가장 시간이 많이 걸린 부분은 Flask decorator를 FastAPI decorator로 바꾸는 일이 아니었다. <code>current_app</code>, <code>g</code>, Flask 전역 설정에 숨어 있던 의존성을 찾아내는 일이 더 중요했다.</p>
<p>DB 연결, storage 설정, 로그 파일 경로를 명시적인 설정 기반 함수로 분리했다. FastAPI는 lifespan에서 연결을 만들고, Flask 호환 함수는 기존 app context에서 동작하도록 유지했다.</p>
<h3 id="7-파일-처리-이관">7. 파일 처리 이관</h3>
<p>파일 업로드는 <code>UploadFile</code>의 stream을 사용하고, local/R2 분기는 storage 계층에서 담당하도록 했다. 라우터에서는 업로드 결과로 object key, 공개 URL, content type, size, checksum을 받아 repository에 메타데이터를 저장한다.</p>
<p>다운로드는 파일 바이트와 <code>Content-Disposition</code> 헤더를 FastAPI <code>Response</code>로 반환하도록 변경했다. 이 덕분에 파일을 브라우저에서 열어버리는 대신 다운로드 응답으로 제어할 수 있었다.</p>
<h3 id="8-현재까지의-결과">8. 현재까지의 결과</h3>
<p>현재 FastAPI는 핵심 API와 프론트엔드 진입 라우팅을 직접 처리할 수 있다. Flask는 아직 제거하지 않았지만, <code>run.py</code>와 기존 blueprint를 호환 경로로 두고 공용 계층부터 Flask 의존성을 줄인 상태다.</p>
<p>다음 글에서는 Docker와 운영 배포 구조, Cloudflare D1/R2 연결, 그리고 Cloudflare Worker를 메인 애플리케이션으로 사용하지 않으면서 Container를 연결하는 방법을 정리할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Eletron App 개발 여정 (1)]]></title>
            <link>https://velog.io/@re_brother/TIL-Eletron-App-%EA%B0%9C%EB%B0%9C-%EC%97%AC%EC%A0%95-1</link>
            <guid>https://velog.io/@re_brother/TIL-Eletron-App-%EA%B0%9C%EB%B0%9C-%EC%97%AC%EC%A0%95-1</guid>
            <pubDate>Fri, 24 Jul 2026 00:05:38 GMT</pubDate>
            <description><![CDATA[<p><strong>Eletron</strong>으로 Desktop App을 만들고자한다.
평소에 이리저리 분주하게 일하는 편이지만 요즘 들어 개발 문서를 작성하다가도 검색하고, 참고해야될 내용을 찾을때도 Chrome 브라우저를 열어 검색해서 URL 붙여넣기, 아키텍쳐 예시 이미지 검색해서 붙여넣기 등..
특히 요즘 개발자들은 <code>md</code> 파일로 문서를 작성하는 트랜드라서 여러 기능이 들어간 markdown edit workdock을 만들어 보고자 한다.</p>
<h3 id="시작하기">시작하기</h3>
<blockquote>
<p><a href="electronforge.io">Eletron Forge</a>는 공식적으로 Vite + TypeScript 템플릿을 제공하고 있기 때문에 아래와 같이 프로젝트 셋팅을 진행합니다.</p>
</blockquote>
<pre><code class="language-bash">&gt; pnpm create electron-app@latest . --template=vite-typescript</code></pre>
<h4 id="시작부터-삐끗">시작부터 삐끗</h4>
<pre><code class="language-bash">PS D:\dev\git\dock&gt; pnpm create electron-app@latest . --template=vite-typescript
Downloading typescript@5.4.5: 5.82 MB/5.82 MB, done
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: C:\Users\user\AppData\Local\pnpm\store\v11
  Virtual store is at:             AppData/Local/pnpm/store/v11/links
[ERR_PNPM_EXOTIC_SUBDEP] Exotic dependency &quot;@electron/node-gyp&quot; (resolved via git-repository) is not allowed in subdependencies when blockExoticSubdeps is enabled
This error happened while installing the dependencies of create-electron-app@7.11.2
 at @electron-forge/cli@7.11.2
 at @electron-forge/core-utils@7.11.2
 at @electron/rebuild@3.7.2
[WARN] GET https://registry.npmjs.org/minimist error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/signal-exit error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/globalthis error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/sprintf-js error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@types%2Fmute-stream error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@types%2Fwrap-ansi error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/type-fest error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/eslint-scope error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/color-name error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/minimatch error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/bluebird error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/glob error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@types%2Fnode error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/es-module-lexer error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/enhanced-resolve error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@webassemblyjs%2Fwasm-parser error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/acorn error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@webassemblyjs%2Fwasm-edit error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@webassemblyjs%2Fast error (unknown). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@xmldom%2Fxmldom error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/get-stream error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/@types%2Fyauzl error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/isbinaryfile error (UND_ERR_DESTROYED). Will retry in 10 seconds. 2 retries left.
[WARN] GET https://registry.npmjs.org/browserslist error (unknown). Will retry in 10 seconds. 2 retries left.
.../mrwr9xx4-bsw                         | Progress: resolved 148, reused 0, downloaded 149, added 0</code></pre>
<pre><code class="language-bash">Exotic dependency &quot;@electron/node-gyp&quot; (resolved via git-repository) is not allowed in subdependencies when blockExoticSubdeps is enabled</code></pre>
<h4 id="원인">원인</h4>
<p>최신 <strong>pnpm</strong>은 기본적으로 <code>blockExoticSubdeps: true</code>라서, 하위 패키지가 Git 저장소 같은 비표준 경로의 의존성을 가져오면 차단한다. 그런데 <strong>Electron Forge</strong> 내부의 <code>@electron/rebuild</code>가 <code>@electron/node-gyp</code>를 Git 의존성으로 사용해서 막힌것이다.</p>
<h4 id="가장-간단한-해결법">가장 간단한 해결법</h4>
<p>프로젝트 생성 단계만 npm으로 진행하자.
현재 폴더에 생성 중 남은 파일이 있는지 먼저 확인한다.</p>
<h4 id="-주의사항">+ 주의사항</h4>
<blockquote>
<p><strong>절대로</strong> 누구처럼 프로젝트 디렉토리에 <code>git init</code>부터 하시면 안됩니다.
프로젝트 root에 <code>.git</code> 디렉토리가 있으면 삭제한 뒤에 프로젝트 설정이 끝나면 다시 설정하시는걸 권장드립니다.</p>
</blockquote>
<pre><code class="language-bash">Get-ChildItem -Force</code></pre>
<p>중간 생성물이 없다면 그대로 실행한다.</p>
<pre><code class="language-bash">npm init electron-app@latest . -- --template=vite-typescript</code></pre>
<p>또는</p>
<pre><code class="language-bash">npx create-electron-app@latest . --template=vite-typescript</code></pre>
<h4 id="생성-후">생성 후</h4>
<pre><code class="language-bash">npm start</code></pre>
<p>Electron 창이 뜨면 Electron 프로젝트 셋팅은 성공</p>
<h3 id="pnpm은-버려야-하냐">pnpm은 버려야 하냐?</h3>
<p>아닙니다. 프로젝트 생성만 npm으로 하고, 생성된 뒤 pnpm으로 전환할 수 있습니다.</p>
<h4 id="생성-완료-후">생성 완료 후</h4>
<pre><code class="language-bash">Remove-Item package-lock.json
pnpm install
pnpm start</code></pre>
<p>다만 같은 오류가 다시 발생할 수 있어서, 당장은 이 프로젝트는 npm으로 통일하는 게 가장 편한 길입니다.</p>
<p><strong>Electron Forge</strong>와 네이티브 모듈 쪽은 npm 기준 자료와 검증 사례가 많아서, MVP 단계에서는 굳이 패키지 매니저 문제까지 같이 해결할 필요까진 없을 것 같습니다.</p>
<h4 id="pnpm을-반드시-유지하고-싶다면">pnpm을 반드시 유지하고 싶다면</h4>
<p>전역 설정을 끄는 방법도 있습니다.</p>
<pre><code class="language-bash">pnpm config set block-exotic-subdeps false --global</code></pre>
<p>확인</p>
<pre><code class="language-bash">pnpm config get block-exotic-subdeps</code></pre>
<p>그다음 다시</p>
<pre><code class="language-bash">pnpm create electron-app@latest . --template=vite-typescript</code></pre>
<p>하지만 이 설정은 pnpm이 제공하는 공급망 보안 기능을 전역으로 비활성화 합니다. 그래서 개인적으로는 추천하는 방법은 아닙니다. <code>blockExoticSubdeps</code>는 하위 의존성이 Git URL이나 직접 tarball URL을 사용하는 것을 막기 위한 설정입니다.</p>
<h4 id="추천하는-진행-방법">추천하는 진행 방법</h4>
<p>현재 Windows 환경에서 개발을 진행하기 때문에 Powershell 기준으로 작성하고 있습니다.
Linux 또는 macOS 환경에서 개발을 진행하실 분들은 해당 내용을 AI Agent에게 질의하여 진행하시면 될 것 같습니다.</p>
<pre><code class="language-bash">cd ~/{workspace_path}/

Get-ChildItem -Force

npx create-electron-app@latest . --template=vite-typescript

npm start</code></pre>
<blockquote>
<p>정리하면 pnpm 최신 보안 정책과 Electron Forge 의존성 구조가 충돌한 상황입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Flask 프로젝트를 FastAPI로 이관하기(2)]]></title>
            <link>https://velog.io/@re_brother/TIL-Flask-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-FastAPI%EB%A1%9C-%EC%9D%B4%EA%B4%80%ED%95%98%EA%B8%B02</link>
            <guid>https://velog.io/@re_brother/TIL-Flask-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-FastAPI%EB%A1%9C-%EC%9D%B4%EA%B4%80%ED%95%98%EA%B8%B02</guid>
            <pubDate>Wed, 22 Jul 2026 07:21:53 GMT</pubDate>
            <description><![CDATA[<h3 id="1-문제-상황">1. 문제 상황</h3>
<ul>
<li>개인 서비스 개발 프로젝트를 Flask로 운영해 왔다.</li>
<li>React 프론트, SQLite/D1, local/R2가 공존하는 상태가 되었다.</li>
<li>PythonAnywhere 이슈와 Cloudflare 이관 고민이 겹쳤다.</li>
<li>그런데 Cloudflare Worker 중심으로 가면 벤더 종속성이 커질 수 있다는 고민이 생겼다.</li>
</ul>
<h3 id="2-선택지-비교">2. 선택지 비교</h3>
<ul>
<li>Flask 유지</li>
<li>Cloudflare Worker 중심 전환</li>
<li>범용 FastAPI 앱으로 재구성</li>
</ul>
<p>여기서 핵심 비교 포인트는:</p>
<ul>
<li>로컬 실행 가능성</li>
<li>타 클라우드 이식성</li>
<li>코드 재사용성</li>
<li>포트폴리오 가치</li>
</ul>
<h3 id="3-현재-구조-분석">3. 현재 구조 분석</h3>
<ul>
<li><code>app/</code>는 Flask 서버</li>
<li><code>frontend/</code>는 React SPA</li>
<li><code>worker-python/</code>는 Cloudflare Worker 실험 코드</li>
<li><code>repository/provider</code> 패턴과 storage 분기 덕분에 완전한 레거시는 아니었다</li>
</ul>
<h3 id="4-fastapi를-선택한-이유">4. FastAPI를 선택한 이유</h3>
<ul>
<li>ASGI 기반</li>
<li>범용 배포</li>
<li>타입 기반 API 설계</li>
<li>문서화/설명력</li>
<li>이미 프로젝트 내부에 일부 FastAPI 경험이 존재</li>
<li>AI 기능 연동 고려</li>
</ul>
<h3 id="5-마이그레이션-전략">5. 마이그레이션 전략</h3>
<ul>
<li>새 FastAPI 앱을 병렬로 세운다</li>
<li>공통 기반부터 분리한다</li>
<li>JSON API부터 옮긴다</li>
<li>파일 처리와 프론트 서빙을 뒤에 옮긴다</li>
</ul>
<h3 id="6-첫-단계에서-한-일">6. 첫 단계에서 한 일</h3>
<ul>
<li>공통 설정 로더 분리</li>
<li>SQLite 초기화 로직의 Flask 의존 완화</li>
<li>FastAPI 앱 골격 생성</li>
<li>migration 문서화 시작</li>
</ul>
<h3 id="7-앞으로-남은-일">7. 앞으로 남은 일</h3>
<ul>
<li>CRUD API 본 이전</li>
<li>업로드/다운로드 이식</li>
<li>프론트 라우팅 이전</li>
<li>seed/init/운영 명령 재정리</li>
</ul>
<h3 id="8-회고-포인트">8. 회고 포인트</h3>
<ul>
<li>프레임워크 교체는 “문법 치환”이 아니라 “구조 재정의”였다</li>
<li>app context 제거가 예상보다 큰 작업 단위였다</li>
<li>이식성을 원하면 런타임보다 코어 아키텍처를 먼저 봐야 했다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Flask 프로젝트를 FastAPI로 이관하기(1)]]></title>
            <link>https://velog.io/@re_brother/TIL-Flask-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-FastAPI%EB%A1%9C-%EC%9D%B4%EA%B4%80%ED%95%98%EA%B8%B01</link>
            <guid>https://velog.io/@re_brother/TIL-Flask-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-FastAPI%EB%A1%9C-%EC%9D%B4%EA%B4%80%ED%95%98%EA%B8%B01</guid>
            <pubDate>Wed, 22 Jul 2026 05:54:33 GMT</pubDate>
            <description><![CDATA[<h2 id="welcome-back">Welcome back</h2>
<p>꽤 긴 시간동안 포스트를 하지 못했다. 이런 저런 이유가 있겠지만 가장 큰 이유 2가지는 업무와 건강상의 이유로 공백이 꽤나 길어졌다.
지난 몇년간 업무상으로 익힌 여러 스킬들이 있고, 예전의 포스팅들을 보며 &quot;내 과거가 이랬었구나. 지금이랑은 많이 다르구나&quot;를 많이 느끼고, 개발이라는 업무 환경만 놓고 보더라도 참 많이 변했음을 느낀다.
그동안 서비스 회사에서 ASP.NET Core, Python 등으로 백엔드 업무를 진행했고, 간간히 요청이 오면 프론트 작업까지 병행하기도 했다. 이후 여러가지 많은 일들이 있었지만 다시 마음을 다잡고 미래의 내가 지금의 나를 다시 돌아볼 수 있는 포스팅을 다시 이어가고자 한다.</p>
<p>최근에는 지인이 개발 요청을 준 사항이 있어서 간단한 프로젝트를 진행중이다. 특정 도메인을 언급할 순 없지만 AI와 협업하는 AI-Human coWorking 시스템을 개발했고 지속적으로 고도화 중이다.</p>
<p>초반 구축 과정에선 간단한 구현만 진행하는 것으로 설계하여 <strong>Flask</strong> 프레임워크를 채택하여 문서/일정/WBS/멤버 관리 등 일반적인 업무에 사용하는 내용만 생각했는데, 해당 내용들을 완성하고 보니 AI Agent를 통해서 기획 문서를 공유하고 AI Agent가 일부 페어 프로그래밍을 진행할 수 있을만한 포인트를 확인하여 최근 AI 서비스에서 많이 채택하는 <strong>FastAPI</strong>로 전체 서비스 이관을 진행할 예정이다.</p>
<h2 id="현재-구조-정리">현재 구조 정리</h2>
<h3 id="1-현재-앱의-성격">1. 현재 앱의 성격</h3>
<p>이 프로젝트는 처음에는 Flask 기반의 개인 서비스 개발 프로젝트로 시작했다.<br>문서, 에셋, WBS, 일정, 멤버 관리 기능이 한 애플리케이션 안에 모여 있으며, 최근에는 Cloudflare D1/R2 이관과 React 프론트 전환이 함께 진행되었다.</p>
<blockquote>
<p>&quot;<strong>왜 CloudFlare를 사용했는가?</strong>&quot;라고 궁금증이 생길 수 있다. 물론 GCP나 AWS 등의 서비스도 검토 대상에 두었지만, 현재 상용화 서비스가 아닌 서비스 구현을 위한 실험적인 단계이기 때문에 인프라 비용이 발생하지 않는 CloudFlare와 Pythonanywhere을 채택했다.</p>
</blockquote>
<p>현재 구조는 크게 세 축으로 볼 수 있다.</p>
<ul>
<li><code>app/</code><ul>
<li>기존 Flask 서버</li>
</ul>
</li>
<li><code>frontend/</code><ul>
<li>React 기반 프론트엔드</li>
</ul>
</li>
<li><code>worker-python/</code><ul>
<li>Cloudflare Python Worker 실험/이행용 서버 코드</li>
</ul>
</li>
</ul>
<h3 id="2-flask-서버의-현재-역할">2. Flask 서버의 현재 역할</h3>
<p>현재 Flask 서버는 아래 역할을 동시에 맡고 있다.</p>
<ul>
<li>JSON API 제공</li>
<li>React 빌드 결과 정적 서빙</li>
<li>로컬 업로드 파일 서빙</li>
<li>SQLite 초기화와 seed 명령 제공</li>
<li>D1/R2를 포함한 하이브리드 런타임 설정 관리</li>
</ul>
<p>즉, 전통적인 서버 역할과 임시 운영 보조 기능이 한곳에 모여 있다.</p>
<h3 id="3-구조적으로-좋은-점">3. 구조적으로 좋은 점</h3>
<ul>
<li><code>repository/provider</code> 패턴이 이미 존재한다.</li>
<li>SQLite와 D1을 추상화하려는 시도가 들어가 있다.</li>
<li>스토리지도 local/R2를 분기할 수 있게 되어 있다.</li>
<li>React 프론트가 분리되어 있어 API 서버 교체가 상대적으로 쉬운 편이다.</li>
</ul>
<p>이 점들은 FastAPI 마이그레이션에 매우 유리하다.</p>
<h3 id="4-현재-구조의-제약">4. 현재 구조의 제약</h3>
<h4 id="4-1-flask-app-context-의존">4-1. Flask app context 의존</h4>
<p>다음 요소들이 Flask 내부 전역 컨텍스트에 기대고 있다.</p>
<ul>
<li><code>current_app</code></li>
<li><code>g</code></li>
<li><code>Blueprint</code></li>
<li><code>jsonify</code></li>
<li><code>send_from_directory</code></li>
<li><code>send_file</code></li>
</ul>
<p>이 의존성은 프레임워크 전환 시 가장 먼저 정리해야 하는 대상이다.</p>
<h4 id="4-2-실행-환경-결합도">4-2. 실행 환경 결합도</h4>
<p>현재 서버는 아래를 한 앱에서 직접 처리한다.</p>
<ul>
<li>로컬 SQLite 파일</li>
<li><code>instance/</code> 기반 로그 파일</li>
<li><code>uploads/</code> 기반 파일 서빙</li>
<li>Cloudflare D1/R2 설정</li>
</ul>
<p>이 자체가 문제는 아니지만, 범용 배포를 목표로 할 때는 실행환경 의존을 좀 더 드러내고 분리해야 한다.</p>
<h4 id="4-3-worker-실험-코드와-본-서버-코드의-분리">4-3. Worker 실험 코드와 본 서버 코드의 분리</h4>
<p><code>worker-python/</code>은 FastAPI 기반이지만 Cloudflare Worker 런타임을 전제로 한다.<br>즉, FastAPI 경험은 이미 일부 축적되어 있지만, 그 코드가 곧바로 “범용 서버”를 의미하지는 않는다.</p>
<h3 id="5-현재-구조를-한-문장으로-요약하면">5. 현재 구조를 한 문장으로 요약하면</h3>
<blockquote>
<p>Flask 중심의 실서비스 서버와 Cloudflare 전환 실험 코드가 공존하고 있으며, 핵심 도메인 로직은 꽤 재사용 가능하지만 프레임워크/런타임 결합을 더 느슨하게 만들어야 한다.</p>
</blockquote>
<hr>
<h2 id="fastapi-전환-의도">FastAPI 전환 의도</h2>
<h3 id="1-왜-지금-flask에서-fastapi로-옮기려고-하는가">1. 왜 지금 Flask에서 FastAPI로 옮기려고 하는가</h3>
<p>이번 전환의 목적은 “Flask가 나쁘다”가 아니다.<br>핵심은 <strong>이 서비스가 특정 런타임이나 호스팅 환경에 잠기지 않게 만드는 것</strong>이다.</p>
<p>현재 운영 리스크와 방향성은 아래처럼 정리된다.</p>
<ul>
<li>PythonAnywhere 서비스 불안정성 이슈가 발생했다.</li>
<li>Cloudflare Worker는 매력적이지만, 장기적으로는 Cloudflare 전용 코드 비중이 커질 수 있다.</li>
<li>로컬 개발, 다른 클라우드, 확장성을 모두 고려하면 범용 ASGI 앱이 더 적합하다.</li>
<li>또한 LLM을 적용 등 AI 개발 방법론을 채택하기엔 <code>Flask</code>보단 <code>FastAPI</code>가 적합하다는 생각을 했다.</li>
</ul>
<h3 id="2-fastapi가-이번-상황에-맞는-이유">2. FastAPI가 이번 상황에 맞는 이유</h3>
<ul>
<li>ASGI 표준 기반이라 이식성이 좋다.</li>
<li>로컬, VPS, Docker, Render, Fly.io, Railway, ECS 등 다양한 환경으로 옮기기 쉽다.</li>
<li>JSON API 중심 앱에 잘 맞는다.</li>
<li>타입 힌트와 문서화 측면에서 사용성이 높다.</li>
<li>이미 프로젝트 내부에 FastAPI 경험(<code>worker-python/</code>)이 일부 있다.</li>
<li>AI 기능을 도입할 부분이 있다.</li>
</ul>
<h3 id="3-이번-마이그레이션의-진짜-목표">3. 이번 마이그레이션의 진짜 목표</h3>
<h4 id="3-1-기술-목표">3-1. 기술 목표</h4>
<ul>
<li>Flask app context 의존 제거</li>
<li>범용 FastAPI 앱 코어 확보</li>
<li>저장소/스토리지/설정 계층의 프레임워크 독립성 강화</li>
<li>JSON API 우선 이전</li>
</ul>
<h4 id="3-2-운영-목표">3-2. 운영 목표</h4>
<ul>
<li>로컬 실행 경로 단순화</li>
<li>특정 호스팅 벤더 의존도 완화</li>
<li>추후 Docker 기반 배포도 가능한 구조 확보</li>
</ul>
<h4 id="3-3-목표">3-3. 목표</h4>
<ul>
<li>“기능 구현”만이 아니라 “서비스 구조 재설계” 경험을 남긴다.</li>
<li>레거시 Flask 앱을 분석하고 ASGI 구조로 이행한 과정을 정리한다.</li>
<li>단순 CRUD가 아니라 저장소 추상화, 스토리지 전략, 런타임 이식성 문제를 함께 설명한다.</li>
</ul>
<h3 id="4-무엇을-하지-않으려는가">4. 무엇을 하지 않으려는가</h3>
<ul>
<li>Cloudflare Worker 전용 구조를 메인 아키텍처로 삼지 않는다.</li>
<li>프레임워크 이름만 바꾸는 기계적 치환은 하지 않는다.</li>
<li>한 번에 모든 코드를 뒤엎는 빅뱅 전환은 피한다.</li>
</ul>
<h3 id="5-전환-전략-한-줄-요약">5. 전환 전략 한 줄 요약</h3>
<blockquote>
<p>기존 Flask 앱 옆에 범용 FastAPI 앱을 세우고, 공통 기반을 분리한 뒤 API부터 단계적으로 이전한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android Layout : February 8, 2022]]></title>
            <link>https://velog.io/@re_brother/TIL-Android-Layout-February-8-2022</link>
            <guid>https://velog.io/@re_brother/TIL-Android-Layout-February-8-2022</guid>
            <pubDate>Tue, 08 Feb 2022 02:16:37 GMT</pubDate>
            <description><![CDATA[<h2 id="layout">Layout</h2>
<h3 id="android-size-unit">Android Size Unit</h3>
<p>안드로이드 위젯의 크기를 지정할 때 사용되는 단위에 대한 정리
<img src="https://images.velog.io/images/re_brother/post/3cd3bbc7-35d5-4ff7-8335-9e39b1c37da7/image.png" alt="">
안드로이드 크기 단위에는 총 6가지(<code>dp</code>, <code>in</code>, <code>mm</code>, <code>pt</code>, <code>px</code>, <code>sp</code>)가 있다.
위젯의 크기를 설정할 때는 <code>wrap_content</code>, <code>match_parent</code> 또는 <code>dp</code>를 사용.
텍스트 사이즈는 <code>sp</code> 단위를 사용하는 것을 권고.</p>
<blockquote>
</blockquote>
<p><strong>1. px (pixel)</strong>
실제 픽셀 수를 뜻합니다. px을 이용하면 해상도가 다른 디바이스마다 실제 크기가 다르게 보이며 심지어 이미지가 깨질 수도 있습니다.
<strong>2. dp / dip (Density Independent Pixel)</strong>
화면의 크기를 기준으로 하며, 어떠한 화면의 해상도에도 같은 비율로 출력해줍니다. px만으로 다양한 디바이스의 해상도를 지원할 수 없기 때문에 자주 쓰입니다.
<strong>3. dpi (Dots per Inch)</strong>
1인치에 들어있는 픽셀 수를 뜻하며, 안드로이드에서는 160을 기본으로 합니다.
<strong>4. sp / sip (Scale Independent Pixel)</strong>
글꼴의 단위로 자주 쓰입니다. dp와 비슷하지만 사용자가 선택한 글꼴에 따라 달라집니다.
<strong>5. in (Inch)</strong>
실제 물리적인 길이(2.54cm)입니다.
<strong>6. mm (milli meter)</strong>
네. 생각하시는 그 mm 맞습니다. 실제 물리적인 길이입니다.
<strong>7. em</strong>
글꼴과 상관 없이 동일한 텍스트의 크기를 출력합니다.<br>
<a href="https://gdbagooni.tistory.com/16">[참조] [Android Studio] 길이 단위 - dp, dpi, px, sp 의 차이점 </a>
<a href="https://offbyone.tistory.com/229">[참조] 안드로이드 크기 단위(dp, sp, pt, px, mm, in)</a></p>
<h3 id="linearlayout">LinearLayout</h3>
<table>
<thead>
<tr>
<th align="center">Properties</th>
<th align="center">Description</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Orientation</td>
<td align="center">horizontal, vertical 정렬 방향 설정</td>
</tr>
<tr>
<td align="center">Weight</td>
<td align="center">가중치 설정</td>
</tr>
</tbody></table>
<h4 id="orientationhorizontal">Orientation(Horizontal)</h4>
<pre><code class="language-xml">&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;
    android:orientation=&quot;horizontal&quot;&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView1&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#ff9966&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView2&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#66ffcc&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView3&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#99ff00&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView4&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#cc00ff&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
&lt;/LinearLayout&gt;</code></pre>
<h4 id="result">Result</h4>
<p><img src="https://images.velog.io/images/re_brother/post/41bfbdee-b311-493d-8af8-dfe633fd1cc7/image.png" alt=""></p>
<h4 id="orientationvertical">Orientation(Vertical)</h4>
<pre><code class="language-xml">&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;
    android:orientation=&quot;vertical&quot;&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView1&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#ff9966&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView2&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#66ffcc&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView3&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#99ff00&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;TextView4&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#cc00ff&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
&lt;/LinearLayout&gt;
</code></pre>
<h4 id="result-1">Result</h4>
<p><img src="https://images.velog.io/images/re_brother/post/2bd92d76-5fc8-4105-8f39-8d22710dda6c/image.png" alt=""></p>
<h3 id="relativelayout">RelativeLayout</h3>
<h4 id="target-base">Target base</h4>
<blockquote>
</blockquote>
<p><code>layout_to[location]Of=&quot;[target]&quot;</code> : <code>[target]</code>의 <code>[location]</code> 지정
<code>location</code> : Right, Left, above, below, StartOf, EndOf</p>
<blockquote>
</blockquote>
<p><code>layout_align[location]=&quot;[target]&quot;</code> : <code>[target]</code>의 <code>[location]</code> 기준 정렬
<code>location</code> : Right, Left, Top, Bottom, Baseline</p>
<h4 id="view-base">View base</h4>
<table>
<thead>
<tr>
<th align="center">Properties</th>
<th align="center">Description</th>
</tr>
</thead>
<tbody><tr>
<td align="center">layout_toLeftOf</td>
<td align="center">뷰(View)를 기준 뷰(Anchor View)의 왼쪽(Left)에 배치</td>
</tr>
<tr>
<td align="center">layout_above</td>
<td align="center">뷰(View)를 기준 뷰(Anchor View)의 위(Above)에 배치</td>
</tr>
<tr>
<td align="center">layout_toRightOf</td>
<td align="center">뷰(View)를 기준 뷰(Anchor View)의 오른쪽(Right)에 배치</td>
</tr>
<tr>
<td align="center">layout_below</td>
<td align="center">뷰(View)를 기준 뷰(Anchor View)의 아래(Below)에 배치</td>
</tr>
<tr>
<td align="center">layout_toStartOf</td>
<td align="center">뷰(View)를 기준 뷰(Anchor View)의 시작(Start)에 배치</td>
</tr>
<tr>
<td align="center">layout_toEndOf</td>
<td align="center">뷰(View)를 기준 뷰(Anchor View)의 끝(End)에 배치</td>
</tr>
<tr>
<td align="center">layout_alignParentLeft</td>
<td align="center">뷰(View)를 부모(Parent) 영역 내에서 왼쪽(Left)에 배치</td>
</tr>
<tr>
<td align="center">layout_alignParentTop</td>
<td align="center">뷰(View)를 부모(Parent) 영역 내에서 위쪽(Top)에 배치</td>
</tr>
<tr>
<td align="center">layout_alignParentRight</td>
<td align="center">뷰(View)를 부모(Parent) 영역 내에서 오른쪽(Right)에 배치</td>
</tr>
<tr>
<td align="center">layout_alignParentBottom</td>
<td align="center">뷰(View)를 부모(Parent) 영역 내에서 아래쪽(Bottom)에 배치</td>
</tr>
<tr>
<td align="center">layout_centerHorizontal</td>
<td align="center">뷰(View)를 부모(Parent) 영역의 가로 방향 가운데 배치</td>
</tr>
<tr>
<td align="center">layout_centerVertical</td>
<td align="center">뷰(View)를 부모(Parent) 영역의 세로 방향 가운데 배치</td>
</tr>
<tr>
<td align="center">layout_centerInParent</td>
<td align="center">뷰(View)를 부모(Parent) 영역의 정 중앙(center)에 배치</td>
</tr>
<tr>
<td align="center">layout_alignParentStart</td>
<td align="center">뷰(View)를 부모(Parent) 영역의 시작 지점(Start)에 배치</td>
</tr>
<tr>
<td align="center">layout_alignParentEnd</td>
<td align="center">뷰(View)를 부모(Parent) 영역의 끝 지점(End)에 배치</td>
</tr>
<tr>
<td align="center">layout_alignLeft</td>
<td align="center">뷰(View)의 왼쪽(Left)을 기준 뷰(View)의 왼쪽(Left)에 맞춤</td>
</tr>
<tr>
<td align="center">layout_alignTop</td>
<td align="center">뷰(View)의 위(Top)를 기준 뷰(View)의 위(Top)에 맞춤</td>
</tr>
<tr>
<td align="center">layout_alignRight</td>
<td align="center">뷰(View)의 오른쪽(Right)를 기준 뷰(View)의 오른쪽(Right)에 맞춤</td>
</tr>
<tr>
<td align="center">layout_alignBottom</td>
<td align="center">뷰(View)의 아래(Bottom)를 기준 뷰(View)의 아래(Bottom)에 맞춤</td>
</tr>
<tr>
<td align="center">layout_alignBaseline</td>
<td align="center">뷰(View)의 폰트 기준선(Baseline)을 기준 뷰(View)의 폰트 기준선(Baseline)에 맞춤</td>
</tr>
</tbody></table>
<h3 id="tablelayout">TableLayout</h3>
<table>
<thead>
<tr>
<th align="center">Properties</th>
<th align="center">Description</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>&lt;TableRow&gt;&lt;/TableRow&gt;</code></td>
<td align="center">테이블 레이아웃의 행을 추가</td>
</tr>
<tr>
<td align="center">weight</td>
<td align="center">가중치 추가</td>
</tr>
<tr>
<td align="center">layout_span=&quot;int&quot;</td>
<td align="center"><code>int</code>만큼 행 병합</td>
</tr>
</tbody></table>
<pre><code class="language-xml">&lt;TableLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;
    &gt;
    &lt;TableRow
    android:layout_height=&quot;0dp&quot;
    android:layout_weight=&quot;1&quot;
    android:background=&quot;#FF0000&quot;&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;Row1 Textview1&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#ff9966&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;Row1 Textview2&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#6600cc&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;Row1 Textview3&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#66ffdd&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:text=&quot;Row1 Textview4&quot;
        android:layout_weight=&quot;1&quot;
        android:background=&quot;#3355cc&quot;
        android:layout_margin=&quot;5dp&quot;
        /&gt;
&lt;/TableRow&gt;
    &lt;TableRow
        android:layout_height=&quot;0dp&quot;
        android:layout_weight=&quot;1&quot;&gt;
        &lt;TextView
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            android:text=&quot;Row2 Textview1&quot;
            android:layout_weight=&quot;1&quot;
            android:background=&quot;#99ff00&quot;
            android:layout_margin=&quot;5dp&quot;
            /&gt;
        &lt;TextView
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            android:text=&quot;Row2 Textview2&quot;
            android:layout_weight=&quot;1&quot;
            android:background=&quot;#cc00ff&quot;
            android:layout_margin=&quot;5dp&quot;
            /&gt;
    &lt;/TableRow&gt;
&lt;/TableLayout&gt;</code></pre>
<h4 id="result-2">Result</h4>
<p><img src="https://images.velog.io/images/re_brother/post/b30c41a0-9b28-441b-8d43-59917c0dc2b2/image.png" alt=""></p>
<h3 id="constraintlayout">ConstraintLayout</h3>
<table>
<thead>
<tr>
<th align="center">Properties</th>
<th align="center">Description</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>layout_constraintCircle=&quot;target&quot;</code></td>
<td align="center">원을 만들 대상</td>
</tr>
<tr>
<td align="center"><code>layout_constraintCircleRadius=&quot;number&quot;</code></td>
<td align="center">원의 크기</td>
</tr>
<tr>
<td align="center"><code>layout_constraintCircleAngle=&quot;radius(number)&quot;</code></td>
<td align="center">원에서 위치가 될 각도 지정</td>
</tr>
</tbody></table>
<pre><code class="language-xml">&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    tools:context=&quot;.MainActivity&quot;&gt;
    &lt;TextView
        android:id=&quot;@+id/t1&quot;
        android:layout_width=&quot;75dp&quot;
        android:layout_height=&quot;75dp&quot;
        android:text=&quot;TextView1&quot;
        app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
        app:layout_constraintRight_toRightOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        android:background=&quot;#ff9966&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;75dp&quot;
        android:layout_height=&quot;75dp&quot;
        android:text=&quot;TextView2&quot;
        app:layout_constraintCircle=&quot;@+id/t1&quot;
        app:layout_constraintCircleRadius=&quot;150dp&quot;
        app:layout_constraintCircleAngle=&quot;140&quot;
        android:background=&quot;#66ffcc&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;75dp&quot;
        android:layout_height=&quot;75dp&quot;
        android:text=&quot;TextView3&quot;
        app:layout_constraintCircle=&quot;@+id/t1&quot;
        app:layout_constraintCircleRadius=&quot;150dp&quot;
        app:layout_constraintCircleAngle=&quot;190&quot;
        android:background=&quot;#99ff00&quot;
        /&gt;
    &lt;TextView
        android:layout_width=&quot;75dp&quot;
        android:layout_height=&quot;75dp&quot;
        android:text=&quot;TextView4&quot;
        app:layout_constraintLeft_toRightOf=&quot;@id/t1&quot;
        app:layout_constraintTop_toTopOf=&quot;@id/t1&quot;
        android:background=&quot;#cc00ff&quot;
        /&gt;
&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</code></pre>
<h4 id="result-3">Result</h4>
<p><img src="https://images.velog.io/images/re_brother/post/85f8dfd7-9389-4dda-8071-118866106795/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Docker Command : December 15, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-Docker-Command-December-15-2021</link>
            <guid>https://velog.io/@re_brother/TIL-Docker-Command-December-15-2021</guid>
            <pubDate>Wed, 15 Dec 2021 07:17:47 GMT</pubDate>
            <description><![CDATA[<h2 id="docker-command">Docker Command</h2>
<blockquote>
<p><code>Docker</code> 사용빈도가 예상보다 높지 않아서 그런지 자주 쓰는 커맨드를 제외하곤 항상 구글링을 한다.
매번 검색해서 찾아서 쓰는것도 나쁘진 않지만 이번 기회에 체계적으로 정리해서 내가 보고싶을때 마다 보기 위해 이 포스팅을 작성한다.
<code>Docker</code>와 <a href="https://hub.docker.com/">Docker Hub</a>를 이용해서 <code>nginx</code> 컨테이너를 생성부터 삭제까지 중간 중간 어떤 커맨드를 사용하는지 적어본다.</p>
</blockquote>
<h3 id="intro">intro</h3>
<p><code>Docker</code>는 기본적으로 Root 권한이 필요하다. <code>Docker</code> 내부에서 Root directory에 접근할 권한이 없기 때문이다.
그래서 대부분 <code>privileged=true</code> 옵션을 사용한다고 하는데, 해당 옵션은 보안 문제가 있을 수 있으니 참고하기 바란다. <code>Docker</code>가 정상적으로 설치되었는지 확인해보자.</p>
<pre><code class="language-bash">$ sudo su
$ docker version</code></pre>
<p>혹시나 <code>Docker</code>를 설치했음에도 불구하고 위 스크립트가 정상적으로 실행되지 않는 경우에는 아래 과정을 진행하시길 바란다.
아래 과정은 현재 사용자 계정을 <code>Docker</code> 그룹에 추가하는 과정이다.</p>
<h4 id="docker-group-확인">Docker group 확인</h4>
<pre><code class="language-bash">$ cat /etc/group | grep docker
docker:x:998:</code></pre>
<p><code>Docker</code> 그룹이 생성되어있지 않다면 아래 명령어로 <code>Docker</code> 그룹을 생성한다.</p>
<pre><code class="language-bash">$ sudo groupadd docker</code></pre>
<h4 id="사용자-추가">사용자 추가</h4>
<p><code>Docker</code> 그룹에 사용자를 추가한다.</p>
<pre><code class="language-bash">$ sudo usermod -aG docker &lt;userName&gt;</code></pre>
<h4 id="docker-group-init">Docker group init</h4>
<p>현재 그룹을 <code>Docker</code> 그룹으로 변경하고 다시 버전을 확인한다.</p>
<pre><code class="language-bash">$ newgrp docker
$ docker version</code></pre>
<h3 id="docker-hub">Docker-hub</h3>
<blockquote>
<p><code>Docker-Hub</code>에는 무수히 많은 컨테이너들이 존재한다. 마이크로소프트와 같은 대형 회사에서 기본적으로 제공해주는 컨테이너, 일반 사용자들이 특수한 목적을 위해 구성해놓은 컨테이너, 개인 목적으로 업로드된 컨테이너 등등..</p>
</blockquote>
<p>이러한 컨테이너들을 어떻게 야무지게 사용하느냐에 따라서 의미없는 에너지 낭비를 피할 수 있다.
예시로 <code>nginx</code> 컨테이너 이미지를 다운로드하여 실행해보도록 하자.</p>
<pre><code class="language-bash">$ docker search
NAME                              DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
nginx                             Official build of Nginx.                        15928     [OK]
jwilder/nginx-proxy               Automated Nginx reverse proxy for docker con…   2101                 [OK]
richarvey/nginx-php-fpm           Container running Nginx + PHP-FPM capable of…   820                  [OK]
jc21/nginx-proxy-manager          Docker container for managing Nginx proxy ho…   288
linuxserver/nginx                 An Nginx container, brought to you by LinuxS…   160
tiangolo/nginx-rtmp               Docker image with Nginx using the nginx-rtmp…   147                  [OK]
...</code></pre>
<p><code>nginx</code>를 검색해본 결과 공식으로 제공되는 이미지가 있고, 그 외에 일반 사용자들이 업로드해놓은 이미지들이 있다.
공식으로 제공되는 <code>nginx</code> 이미지를 다운로드하자.</p>
<h4 id="docker-images">docker images</h4>
<pre><code class="language-bash">$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE</code></pre>
<p><code>docker images</code> 명령어를 통해 현재는 보유한 이미지를 확인할 수 있다.</p>
<h4 id="image-download-directory">image download directory</h4>
<p>다운로드된 이미지는 <code>/var/lib/docker/overlay2</code>에 저장된다.</p>
<h4 id="image-download">image download</h4>
<p>컨테이너 이미지를 다운로드 하는 커맨드는 <code>docker pull &lt;IMAGENAME&gt;:&lt;VERSION&gt;</code>이다. 버전을 생략할 경우 최신 버전으로 다운로드된다.</p>
<pre><code class="language-bash">$ docker pull nginx

Using default tag: latest
latest: Pulling from library/nginx
e5ae68f74026: Pull complete
21e0df283cd6: Pull complete
ed835de16acd: Pull complete
881ff011f1c9: Pull complete
77700c52c969: Pull complete
44be98c0fab6: Pull complete
Digest: sha256:9522864dd661dcadfd9958f9e0de192a1fdda2c162a35668ab6ac42b465f0603
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest</code></pre>
<p>따로 <code>&lt;VERSION&gt;</code>을 선택하지 않았기 때문에 latest 버전으로 설치된다.</p>
<pre><code class="language-bash">$ docker images

REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
nginx        latest    f652ca386ed1   13 days ago   141MB

$ sudo ls -alFh /var/lib/docker/overlay2/
total 36K
drwx--x---  9 root root 4.0K Dec 16 02:02 ./
drwx--x--- 13 root root 4.0K Dec 14 00:53 ../
drwx--x---  3 root root 4.0K Dec 16 02:02 11a95abfa3e2b45cab9113eb447d98abc70b6e1a84f9435cea419c811f147b1c/
drwx--x---  4 root root 4.0K Dec 16 02:02 2d471323df269aeaff19c810a186cf36e48e3034031cbab8c03ed89e6302fb8e/
drwx--x---  4 root root 4.0K Dec 16 02:02 8235dae3e16fe4df2032d1ab2fd49c3215e173109743502715bd1f61b49c2bbe/
drwx--x---  4 root root 4.0K Dec 16 02:02 8d5040887ffef2eaeae14466a20691aaa2bb220fcc1b0e368b361ee2799b79c6/
drwx--x---  4 root root 4.0K Dec 16 02:02 9ea30971c486bc54be8175e72cb8dac86e08928a4a49dace099748bdd4da7e74/
drwx--x---  4 root root 4.0K Dec 16 02:02 c7422c3c00ac2ff7487fd66d50407f796e316436e7779fede1e3244bbcf80c1b/
drwx------  2 root root 4.0K Dec 16 02:02 l/</code></pre>
<p><code>docker images</code>, <code>sudo ls -alFh /var/lib/docker/overlay2</code>로 컨테이너 이미지 정상 다운로드를 확인한다. (<del>다운로드가 안되는 경우는 드물다. 그냥 알아두기만 하자.</del>)</p>
<hr>
<p>작성중...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Ubuntu Docker Install : December 14, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-Ubuntu-Docker-Install-December-14-2021</link>
            <guid>https://velog.io/@re_brother/TIL-Ubuntu-Docker-Install-December-14-2021</guid>
            <pubDate>Tue, 14 Dec 2021 04:24:16 GMT</pubDate>
            <description><![CDATA[<h2 id="ubuntu-docker-install">Ubuntu Docker install</h2>
<blockquote>
<p><code>AWS EC2 Ubuntu</code> 서버에 <code>ASP.NET Core</code> 업로드 테스트를 진행중이다.
서버는 <code>Ubuntu 18.04 LTS</code>이며, 모든 작업은 CLI 환경에서 진행했다.</p>
</blockquote>
<h3 id="why-docker">Why Docker?</h3>
<blockquote>
<p><strong>도커(Docker)</strong>는 리눅스의 응용 프로그램들을 소프트웨어 컨테이너 안에 배치시키는 일을 자동화하는 오픈 소스 프로젝트이다. - 위키백과</p>
</blockquote>
<p>그렇게 어려운 말로 적혀있는건 아닌데, 모르는 사람이 들으면 이게 무슨 소리인가 싶을거다.
어느정도 현실에 빗대어 이야기하자.</p>
<p><strong>어떤 API 서버를 구축했고, 클라우드 서비스를 통해 배포 해야한다.</strong></p>
<p>이 상황에서 모든 클라우드 플랫폼이 동일한 환경을 가지고 있지 않고, 개발 당시의 환경과도 다르기 때문에 개발된 환경과 동일한 환경이 클라우드상에도 설정되어야 한다.
서비스가 정상적으로 운영되기 위한 모든 환경을 규격화하는 것이다. 이렇게 규격화된 환경은 차후 다른 서비스를 개발하거나, 유사한 서비스를 개발할 때 시간(자원)을 아껴주는 역할을 한다.</p>
<p><code>Docker</code>의 가장 큰 장점은 <strong>변경 불가능한 인프라(Immutable Infrastructure)</strong>이다. 소프트웨어는 자주 수정된다면 여러가지 오류가 발생한다. 특히 변경에 따른 이력관리도 매우 중요한데 <code>Docker</code>는 서버를 구축한 이후 변경이나 업데이트를 할 수 없다.
위와 같은 이유로 인해 <code>Docker</code>는 업데이트 개념이 아닌 서버를 통째로 바꾸는 개념이다. 서버를 업데이트 하기 위해 새로운 도커 이미지를 업데이트해서 컨테이너에 담는다.
불편하다고 생각이 들 수 있겠지만 자잘한 소프트웨어 업데이트로 인해 머리를 쥐어 뜯는것보다 변경하기 위한 이미지를 올리는 것으로 배포를 끝낼 수 있다.</p>
<h3 id="install-docker-engine-on-ubuntu">Install Docker Engine on Ubuntu</h3>
<p>아래 방법은 <code>repository</code>를 이용한 설치법이다. <code>package</code>를 이용하여 <code>Docker</code>를 설치하려면 <a href="https://docs.docker.com/engine/install/ubuntu/#install-from-a-package">Install from a package - Docker Engine</a> 를 참고하길 바란다.</p>
<h4 id="uninstall-old-versions">Uninstall old versions</h4>
<p>혹시 이전에 설치했던 <code>Docker</code>가 있을 경우를 대비하여 사전에 지워주도록 하자.</p>
<pre><code class="language-bash">$ sudo apt-get remove docker docker-engine docker.io containerd runc

Reading package lists... Done
Building dependency tree
Reading state information... Done
Package &#39;docker-engine&#39; is not installed, so not removed
Package &#39;docker&#39; is not installed, so not removed
Package &#39;containerd&#39; is not installed, so not removed
Package &#39;docker.io&#39; is not installed, so not removed
Package &#39;runc&#39; is not installed, so not removed
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.</code></pre>
<h4 id="set-up-the-repository">Set up the repository</h4>
<ol>
<li><p><code>apt</code>를 업데이트하고, <code>https</code>를 통해 리포지토리를 쉽게 사용할 수 있도록 패키지를 설치한다.</p>
<pre><code class="language-bash">$ sudo apt-get update
$ sudo apt-get install ca-certificates curl gnupg lsb-release</code></pre>
</li>
<li><p><code>Docker</code> 공식 GPG Key를 추가한다.
```bash
$ curl -fsSL <a href="https://download.docker.com/linux/ubuntu/gpg">https://download.docker.com/linux/ubuntu/gpg</a> | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg</p>
</li>
</ol>
<p>-----BEGIN PGP PUBLIC KEY BLOCK-----</p>
<p>mQINBFit2ioBEADhWpZ8/wvZ6hUTiXOwQHXMAlaFHcPH9hAtr4F1y2+OYdbtMuth
lqqwp028AqyY+PRfVMtSYMbjuQuu5byyKR01BbqYhuS3jtqQmljZ/bJvXqnmiVXh
38UuLa+z077PxyxQhu5BbqntTPQMfiyqEiU+BKbq2WmANUKQf+1AmZY/IruOXbnq
...
YT90qFF93M3v01BbxP+EIY2/9tiIPbrd
=0YYh
-----END PGP PUBLIC KEY BLOCK-----</p>
<pre><code>
추가된 gpg key는 `/usr/share/keyrings`에 저장된다. 열어볼 필요 없다. 어차피 읽지도 못한다.

3. `stable` 리포지토리를 설정하는 경우 아래와 같이 작성하고, `nightly` or `test` 버전을 원하는 경우 `stable` 대신에 `nightly` or `test`를 넣어주면 된다.
```bash
$ echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</code></pre><h4 id="install-docker-engine">Install Docker Engine</h4>
<ol>
<li>위에서 설정한 리포지토리를 업데이트<pre><code class="language-bash">$ sudo apt-get update
</code></pre>
</li>
</ol>
<p>Hit:1 <a href="http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu">http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu</a> bionic InRelease
Hit:2 <a href="http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu">http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu</a> bionic-updates InRelease
Hit:3 <a href="http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu">http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu</a> bionic-backports InRelease
Get:4 <a href="https://download.docker.com/linux/ubuntu">https://download.docker.com/linux/ubuntu</a> bionic InRelease [64.4 kB]
Get:5 <a href="https://download.docker.com/linux/ubuntu">https://download.docker.com/linux/ubuntu</a> bionic/stable amd64 Packages [22.5 kB]
Get:6 <a href="https://download.docker.com/linux/ubuntu">https://download.docker.com/linux/ubuntu</a> bionic/nightly amd64 Packages [59.8 kB]
Get:7 <a href="http://security.ubuntu.com/ubuntu">http://security.ubuntu.com/ubuntu</a> bionic-security InRelease [88.7 kB]
Fetched 236 kB in 2s (153 kB/s)
Reading package lists... Done</p>
<pre><code>
2. `docker-ce`, `docker-ce-cli`, `containerd.io`를 설치한다.
```bash
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
Reading package lists... Done
Building dependency tree
Reading state information... Done
...
</code></pre><p>설치가 완료되었고, <code>stable repository</code>에서 가장 최신 버전으로 설치되었다.
특정 버전을 설치해야되는 경우에는 <code>apt-cache</code> 명령어를 사용해서 원하는 버전을 설치한다.</p>
<pre><code class="language-bash">$ apt-cache madison docker-ce

 docker-ce | 5:20.10.12~3-0~ubuntu-bionic | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages
 docker-ce | 5:20.10.11~3-0~ubuntu-bionic | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages
 docker-ce | 5:20.10.10~3-0~ubuntu-bionic | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages
 ...
 docker-ce | 18.06.1~ce~3-0~ubuntu | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages
 docker-ce | 18.06.0~ce~3-0~ubuntu | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages
 docker-ce | 18.03.1~ce~3-0~ubuntu | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages

 $ sudo apt-get install docker-ce=&lt;VERSION_STRING&gt; docker-ce-cli=&lt;VERSION_STRING&gt; containerd.io</code></pre>
<p>만약 <code>5:20.10.10~3-0~ubuntu-bionic</code> 버전을 설치하고 싶은 경우</p>
<pre><code class="language-bash">$ sudo apt-get install docker-ce=5:20.10.10~3-0~ubuntu-bionic docker-ce-cli=5:20.10.10~3-0~ubuntu-bionic containerd.io</code></pre>
<p><code>&lt;VERSION_STRING&gt;</code>에 원하는 버전을 입력해서 설치하면 된다.</p>
<h4 id="test">Test</h4>
<p>동작하는지 확인은 해봐야하니까 <code>hello-world</code> 컨테이너를 실행해보자. 로컬에 <code>hello-world</code>라는 컨테이너가 없으니 <code>Docker Hub</code>에서 자동으로 컨테이너를 다운받아 실행된다.</p>
<pre><code class="language-bash">$ sudo docker run hello-world
Unable to find image &#39;hello-world:latest&#39; locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:cc15c5...4f685
Status: Downloaded newer image for hello-world:latest</code></pre>
<p>제대로 실행된건지 확인하기 위해 <code>docker ps -a</code> 명령어 실행(<code>sudo</code> 권한으로 해야함)</p>
<pre><code class="language-bash">$ sudo docker ps -a
CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                      PORTS     NAMES
e42a5f4182f1   hello-world   &quot;/hello&quot;   29 seconds ago   Exited (0) 29 seconds ago             competent_joliot
</code></pre>
<pre><code class="language-bash">$ systemctl status docker</code></pre>
<p><img src="https://images.velog.io/images/re_brother/post/6cbb6f8d-c55a-43ef-9dd4-41244a88bd98/image.png" alt=""></p>
<p>정상적으로 실행되고 있음을 확인할 수 있다.</p>
<h3 id="ending">Ending</h3>
<p>일주일에 적어도 한 번은 적어보려고 노력하는데 그게 생각처럼 쉽지가 않다.</p>
<blockquote>
<p><strong>앞으로 기록할 것들</strong></p>
</blockquote>
<ul>
<li>AWS Lambda에 업로드한 코드 수정하기(json parsing)</li>
<li>dotnet publish 패키지 증발 현상</li>
<li>AWS Lambda 함수와 AWS API Gateway 연동하기</li>
<li><del>AWS EC2 ubuntu에 Docker 환경 구축하기</del> + ASP.NET Core Docker Image</li>
</ul>
<p>꼼꼼하게 확인해가면서 배우는 습관을 들여야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] AWS Lambda .NET Core : December 9, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-AWS-Lambda-.NET-Core-December-9-2021</link>
            <guid>https://velog.io/@re_brother/TIL-AWS-Lambda-.NET-Core-December-9-2021</guid>
            <pubDate>Thu, 09 Dec 2021 06:55:08 GMT</pubDate>
            <description><![CDATA[<h2 id="aws-lambda-dockernet">AWS Lambda Docker(.NET)</h2>
<blockquote>
<p>AWS Lambda에 <code>.NET Core</code> 프로젝트를 배포하는 과정을 적어볼 예정이다.</p>
</blockquote>
<h3 id="aws-toolkit-for-visual-studio">AWS Toolkit for Visual Studio</h3>
<p>AWS 공식 문서를 참고하여 진행한다. AWS에서는 Visual Studio 2017/2019에 대한 공식 문서만 제공하고 있다.
<code>Visual Studio 2022(이하 VS2022)</code>는 아직 공식 문서가 작성되진 않았지만, <code>VS2022</code>를 지원하는 AWS Toolkit Preview는 제공되고 있으니 일단 설치해보기로 한다.</p>
<p><a href="https://aws.amazon.com/ko/visualstudio/">&gt; AWS Toolkit for Visual Studio Download</a></p>
<hr>
<h3 id="aws-toolkit-download--install">AWS Toolkit download &amp; install</h3>
<p><img src="https://images.velog.io/images/re_brother/post/7445c3ea-e7b7-4333-a173-cba61dda141a/image.png" alt="">
위에서 언급한 내용대로 <code>VS2022</code>로 설치한다.
<img src="https://images.velog.io/images/re_brother/post/662e5400-f202-47cf-9133-5b2f56966f02/image.png" alt="">
[install] 버튼 1회만 클릭하면 그리 오래걸리지 않는 시간내에 설치가 완료된다. 솔직히 카톡 PC버전 설치보다 쉬운듯.</p>
<hr>
<h3 id="basic-aws-lambda-project">Basic AWS Lambda Project</h3>
<p>설치가 완료된 상태에서 <code>VS2022</code>를 실행하게 되면 AWS 관련 템플릿들이 추가된 것을 볼 수 있다. <p style="font-size: 1px">징그럽게 많다..</p></p>
<p><img src="https://images.velog.io/images/re_brother/post/ba7efd3a-5db0-4a47-804c-ff9398551207/image.png" alt="">
<code>AWS Lambda Project (.NET Core - C#)</code>를 선택하고 프로젝트의 이름과 위치를 지정한다.</p>
<p><img src="https://images.velog.io/images/re_brother/post/4f2e1756-c3bb-4a2d-8919-b90ca7b42b77/image.png" alt="">
<strong>만들기</strong>를 클릭하면 위와 같이 Blueprint를 선택하는 창이 뜨는데 당황하지말고 <code>Empty Function</code>을 선택하면 된다.</p>
<p><img src="https://images.velog.io/images/re_brother/post/66a430d6-1a21-4877-b5a0-ce1889cf03d4/image.png" alt="">
<a href="https://console.aws.amazon.com/iamv2/home">AWS IAM 콘솔</a>에서 발급받은 Access Key와 Secret Key를 입력하고 Save 하면 된다. 혹시나 Secret Key를 분실한 경우 새로 발급받아야함.</p>
<p><a href="https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/keys-profiles-credentials.html">[참고] AWS 자격 증명 프로필 생성 문서</a></p>
<p><img src="https://images.velog.io/images/re_brother/post/29f9bd6e-a8e7-41ba-8196-7514a3e030a4/image.png" alt=""></p>
<p>정상적으로 연결되었다면 좌측에 AWS Explorer에 서비스들이 출력된다. <strong>혹시나</strong> 여기서도 AWS Explorer가 생성되지 않았다면 [보기] -&gt; [AWS Explorer] 또는 Ctrl + K, A 숏컷을 사용하면 된다.</p>
<p>솔루션 탐색기에 <code>aws-lambda-tools-defaults.json</code>, <code>Function.cs</code> 파일이 있는데 해당 파일 설명은 AWS 공식 문서를 보는게 좋을 것 같다.</p>
<blockquote>
<h3>aws-lambda-tools-defaults.json</h3><p></p>
프로젝트의 일부로 생성된 파일을 보여줍니다. Lambda 도구가 기본적으로 읽는 이 파일의 필드를 사용하여 빌드 옵션을 설정할 수 있습니다. Visual Studio의 프로젝트 템플릿에는 기본값이 있는 이러한 필드가 많이 포함되어 있습니다. function-handler 필드는 Lambda 함수가 실행될 때 실행되는 메서드를 지정합니다.
함수 핸들러를 지정하는 경우 필드는 게시 마법사에 미리 채워져 있습니다. 함수, 클래스 또는 어셈블리의 이름을 바꾸면 aws-lambda-tools-defaults.json 파일의 필드도 업데이트해야 합니다.
</blockquote>
<pre><code class="language-json">{
  &quot;Information&quot;: [
    &quot;This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.&quot;,
    &quot;To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.&quot;,
    &quot;dotnet lambda help&quot;,
    &quot;All the command line options for the Lambda command can be specified in this file.&quot;
  ],
  &quot;profile&quot;: &quot;&quot;,
  &quot;region&quot;: &quot;&quot;,
  &quot;configuration&quot;: &quot;Release&quot;,
  &quot;framework&quot;: &quot;netcoreapp3.1&quot;,
  &quot;function-runtime&quot;: &quot;dotnetcore3.1&quot;,
  &quot;function-memory-size&quot;: 256,
  &quot;function-timeout&quot;: 30,
  &quot;function-handler&quot;: &quot;AWSLambda_basic::AWSLambda_basic.Function::FunctionHandler&quot;
}</code></pre>
<blockquote>
<h3>Function.cs</h3><p></p>
Lambda 함수로 노출할 C# 함수를 정의합니다. 이것은 FunctionHandlerLambda 함수가 실행될 때 실행되는 Lambda 기능입니다. 이 프로젝트에는 입력 텍스트 FunctionHandler를 호출하는 하나의 함수가 정의되어 ToUpper()있습니다.
</blockquote>
<pre><code class="language-cs">// Function.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function&#39;s JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace AWSLambda_basic
{
    public class Function
    {

        /// &lt;summary&gt;
        /// A simple function that takes a string and does a ToUpper
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;input&quot;&gt;&lt;/param&gt;
        /// &lt;param name=&quot;context&quot;&gt;&lt;/param&gt;
        /// &lt;returns&gt;&lt;/returns&gt;
        public string FunctionHandler(string input, ILambdaContext context)
        {
            return input?.ToUpper();
        }
    }
}
</code></pre>
<hr>
<h4 id="test">Test</h4>
<p>가장 기본적인 테스트만 진행할 계획이였기 때문에 <code>Function.cs</code>에 기본적으로 작성되어있는 <code>ToUpper()</code> 기능으로 테스트를 진행할 예정이다.</p>
<p>AWS Explorer에서 <code>AWS Lambda</code> 우클릭 후 <code>Publish to AWS Lambda</code> 선택
<img src="https://images.velog.io/images/re_brother/post/0bdf488e-4f8f-4c6d-891d-8a37e14c0e2d/image.png" alt="">
실제로 프로젝트 진행시에는 .NET Core v3.1을 사용할 계획은 없지만 이번에는 반항하지 않고 AWS 공식 문서에서 시키는데로 따라해본다.
<code>VS2019</code>, <code>VS2017</code>과는 아주 조금 다른 창이 뜨는데 엄청 많이 다른건 아니니까 요령껏 작성했다.
<img src="https://images.velog.io/images/re_brother/post/4f3768a1-e2f3-4d0b-9378-7cda98e83570/image.png" alt="">
다음 페이지에선 크게 만질만한 옵션이 없다 <code>Role Name</code>만 <code>AWSLambdaBasicExecutionRole</code>로 변경해주고 업로드한다.</p>
<p>업로드가 정상적으로 완료되면 업로드 윈도우가 자동으로 종료되고, AWS Explorer를 보면 <code>AWS Lambda</code>에 방금 추가한 Function이 추가되어있다.</p>
<h4 id="error--could-not-find-the-required-assemblydepsjson">Error : Could not find the required &#39;{Assembly}.deps.json&#39;</h4>
<pre><code class="language-json">{
  &quot;errorType&quot;: &quot;LambdaException&quot;,
  &quot;errorMessage&quot;: &quot;Could not find the required &#39;AWSLambda_basic.deps.json&#39;.
                  This file should be present at the root of the deployment package.&quot;
}
</code></pre>
<p>새롭게 생성된 Function 윈도우에서 샘플 테스트를 진행하는데 보고 따라했음에도 에러가 생긴다.
<strong>참 신기한 세상이다.</strong><p style="font-size: 1px">내 머리가 레전드인건지..</p></p>
<p>해당 오류는 <code>*.deps.json</code> 파일이 없어서 생기는 오류인데, <code>*.deps.json</code> 파일은 프로젝트를 publish하게 되면 <code>.\bin\Debug\\{runtime}\publish\</code> 디렉터리에 생성되는 파일이다.
AWS Lambda에 업로드하기 전에 Visual Studio에서 <code>dotnet publish</code> 후 파일 생성까지 확인.</p>
<hr>
<h4 id="해결">해결</h4>
<p><code>dotnet publish</code>한 프로젝트를 다시 <code>AWS Lambda</code>에 업로드했지만 해당 오류는 해결되지 않았다.
이래저래 테스트를 하던 중 <code>AWS Lambda</code> 업로드 중 <code>Rold Name</code>을 기존 <code>AWSLambdaBasicExecutionRole</code>에서 <code>AWSLambdaRole (Default policy for AWS Lambda Service Role.)</code>으로 변경했고, <code>AWS Lambda</code>에 업로드된 <code>ToUpper()</code> 함수는 정상적으로 동작했다.
<img src="https://images.velog.io/images/re_brother/post/1c7d8a6e-56a8-40e4-8f7e-199fe3ea2fbd/image.png" alt=""></p>
<hr>
<h3 id="ending">Ending</h3>
<p>한동안 기록없이 코드만 치다보니 감이 떨어진듯하다. 습관이 이렇게 무섭다.</p>
<blockquote>
<p><strong>앞으로 기록할 것들</strong></p>
</blockquote>
<ul>
<li><code>AWS Lambda</code>에 업로드한 코드 수정하기(json parsing)</li>
<li><code>dotnet publish</code> 패키지 증발 현상</li>
<li><code>AWS Lambda</code> 함수와 <code>AWS API Gateway</code> 연동하기</li>
<li><code>AWS EC2 ubuntu</code>에 <code>Docker</code> 환경 구축하기 + <code>ASP.NET Core Docker Image</code></li>
</ul>
<p>기록할 일이 많다는건 좋은 상황인지, 아닌지.</p>
<p><a href="https://devstarsj.github.io/cloud/2016/12/03/AwsLambda.CSharp/">[참고] AWS Lambda에 C# Handler 만들기 - devstarsj</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] C# : December 3, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-C-December-3-2021</link>
            <guid>https://velog.io/@re_brother/TIL-C-December-3-2021</guid>
            <pubDate>Fri, 03 Dec 2021 05:45:22 GMT</pubDate>
            <description><![CDATA[<h2 id="why-c">Why C#?</h2>
<blockquote>
<p>새로운 회사에 이직하게 되었다.
자체 서비스를 C#으로 운영하고 있기에 적응해야 한다.
아마 개인적으로 스터디하는 내용을 이 게시물에 계속 추가하지 않을까 싶다.
정확하지 않은 내용이 있다면 따로 표시해놓고 수정할 계획이다.</p>
</blockquote>
<h3 id="datagrid">DataGrid</h3>
<blockquote>
<p><code>Grid</code>는 컨트롤들을 담지만 <code>DataGrid</code>는 사용자 정의 가능한 표 형태로 데이터를 표시하는 컨트롤로 행/열에 데이터를 표시하는 유연한 방법을 제공</p>
</blockquote>
<center>

<table>
<thead>
<tr>
<th>값</th>
<th align="center">범위</th>
</tr>
</thead>
<tbody><tr>
<td><code>DataGridTextColumn</code></td>
<td align="center">Text 지원</td>
</tr>
<tr>
<td><code>DataGridCheckBoxColumn</code></td>
<td align="center">CheckBox 지원</td>
</tr>
<tr>
<td><code>DataGridTemplateColumn</code></td>
<td align="center">나머지 컨트롤</td>
</tr>
</tbody></table>
</center>

<p><code>DataGridColumn</code>에 추가할 수 있는 3가지 Column이다. 많이 사용하는 <code>Text</code>와 <code>CheckBox</code> Column은 따로 지원하지만, 나머지는 <code>DataGridTemplateColumn</code>을 사용해야 한다.</p>
<p><strong>결론 : <code>Text</code>, <code>CheckBox</code>가 아닌 원하는 컨트롤을 넣고 싶을 때 <code>DataGridTemplateColumn</code>을 사용한다.</strong></p>
<p><a href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=julymorning4&amp;logNo=222073855237">[참고] WPF DataGrid 예제 - 오라클자바커뮤니티(OJC.ASIA) // C# 관련된 포스팅이 꽤 있는편이다.
따로 시간내서 하나씩 보면 좋을듯</a></p>
<h3 id="question-mark">Question mark</h3>
<p>Python, Javascript와는 다르게 변수 타입에 <code>?</code>를 붙여 사용하는 경우가 많다.</p>
<pre><code class="language-cs">public partical class ExampleModel
{
  private int? a = null; // OK
  private int b = null; // error since b is not nullable
}</code></pre>
<p>위와 같이 C#에서 <code>int?</code>, <code>bool?</code>, <code>DateTime?</code>와 같은 <code>T?</code>의 표현은 .NET의 <code>Nullable&lt;T&gt;</code>와 같은 표현이다. <code>Nullable&lt;T&gt;</code> 구조체는 값을 가지고 있는지 체크한다.
<code>Nullable</code> 타입과 연관되어 자주 사용되는 <code>??</code> 연산자는 <code>??</code> 앞의 파라미터가 <code>NULL</code>인 경우 연산자 뒤의 값을 할당한다.</p>
<pre><code class="language-cs">double _Sum = 0;
DateTime _Time;
bool? _Selected;

public void CheckInput(int? i, double? d, DateTime? time, bool? selected)
{
    if (i.HasValue &amp;&amp; d.HasValue)
        this._Sum = (double)i.Value + (double)d.Value;

    // time valid check
    if (!time.HasValue)
        throw new ArgumentException();
    else
        this._Time = time.Value;

    // if selected is NULL &gt; false
    this._Selected = selected ?? false;
}</code></pre>
<p><strong><code>Nullable</code> 타입이 필요한 이유</strong> : SQL 서버 테이블에서 NULL을 허용하는 컬림이 있을 때, 테이블의 <code>NULL</code> 속성을 표현하기 위해서</p>
<h3 id="binding">Binding</h3>
<blockquote>
<p><code>Binding</code>은 데이터 또는 속성(프로퍼티)을 서로 연결하여 동적으로 변환 및 참조할 수 있도록 한다. <strong>한 엘리먼트의 속성</strong>을 <strong>다른 엘리먼트의 속성</strong>, 또는 <strong>데이터</strong>와 연결하는 과정
<code>Binding</code>을 통해 간단하게 서로 값들을 참조하여 동적으로 변경시킬 수 있고, 긴 처리 없이 간단하게 리스트에 <code>DataTable</code>을 표시하는 등 장점이 많기 때문에 WPF 작업시에 <code>Binding</code>은 필수적으로 사용된다.</p>
</blockquote>
<p><code>Binding</code>은 XAML측, Code-behind(C#)측에서 하는 방법이 존재한다.</p>
<h4 id="xaml-binding">XAML Binding</h4>
<pre><code class="language-xaml">&lt;!-- XAML Binding --&gt;
&lt;!-- Controll TextBox to ScrollBar --&gt;
&lt;ScrollBar Name=&quot;scrollBar&quot; Orientation=&quot;Horizontal&quot; Maximum=&quot;100&quot; LargeChange=&quot;10&quot;
    SmallChange=&quot;1&quot; Value=&quot;{Binding ElementName=textbox, Path=Text, Mode=OneWay}&quot;/&gt;
&lt;TextBox Name=&quot;textbox&quot; VerticalAlignment=&quot;Top&quot;/&gt;</code></pre>
<h4 id="code-behind-binding">Code-behind Binding</h4>
<pre><code class="language-cs">int i = 0;
private void btn0_Click(object sender, RoutedEventArgs e)
{
    i++;
    Binding binding = new Binding();
    binding.Source = slider;   // Slider
    binding.Path = new PropertyPath(&quot;Value&quot;);   // Property of Slider
    if (i % 5 == 0) binding.Mode = BindingMode.Default;
    if (i % 5 == 1) binding.Mode = BindingMode.OneTime;
    if (i % 5 == 2) binding.Mode = BindingMode.OneWay;
    if (i % 5 == 3) binding.Mode = BindingMode.OneWayToSource;
    if (i % 5 == 4) binding.Mode = BindingMode.TwoWay;
    lbl.SetBinding(ContentProperty, binding);

    lblMode.Content = binding.Mode;
}

private void btn1_Click(object sender, RoutedEventArgs e)
{
    if(lbl.Content != null) lbl.Content = (double)lbl.Content + 1;
}
</code></pre>
<h4 id="binding-mode">Binding Mode</h4>
<center>

<table>
<thead>
<tr>
<th>Mode</th>
<th align="center">의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>OneTime</code></td>
<td align="center">최초 1회만 값을 전달</td>
</tr>
<tr>
<td><code>OneWay</code></td>
<td align="center">A를 B에 바인딩했다면, B의 값이 변경되면 A의 값도 변경</td>
</tr>
<tr>
<td><code>OneWayToSource</code></td>
<td align="center">OneyWay와 반대 / B를 A에 바인딩, A의 값이 변경되면 B의 값도 변경</td>
</tr>
<tr>
<td><code>TwoWay</code></td>
<td align="center">하나가 변경되면 다른 하나도 따라서 변경</td>
</tr>
</tbody></table>
</center>

<p><a href="https://blog.naver.com/alphak4000/130123447028">[참고] Binding Mode Example (XAML)</a></p>
<p><a href="https://insurang.tistory.com/342">[참고] Binding Mode Example (Code-behind)</a></p>
<h3 id="reference-site">Reference Site</h3>
<p><a href="https://chashtag.tistory.com/1">CHashtag Blog</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Docker : June 1, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-Docker-June-1-2021</link>
            <guid>https://velog.io/@re_brother/TIL-Docker-June-1-2021</guid>
            <pubDate>Tue, 01 Jun 2021 07:55:56 GMT</pubDate>
            <description><![CDATA[<h2 id="docker">Docker</h2>
<h3 id="got-permission-denied-while-trying-to-connect-to-the-docker-daemon-socket-at-">Got permission denied while trying to connect to the Docker daemon socket at ...</h3>
<pre><code class="language-shell">$ docker ps
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json: dial unix /var/run/docker.sock: connect: permission denied</code></pre>
<p><code>$ docker ps</code> 명령어를 입력했을때 마주치게 되는 권한 에러로 일반적으로 도커 소켓에 접근할 권한이 없기 때문에 발생한다. 이런 경우 <code>docker ps</code> 앞에 <code>sudo</code>를 붙여주면 해결이 되지만, 사용자 계정에서도 도커에 대한 접근이 가능하게 할 수 있도록 <code>docker</code> 그룹에 사용자를 추가한다.</p>
<pre><code class="language-shell">$ sudo usermod -aG docker $USER_NAME
$ sudo su - $USER_NAME</code></pre>
<p>위와 같이 적용하게 되면 <code>sudo</code> 명령어 없이도 정상적으로 <code>docker</code> 명령어를 사용할 수 있다.</p>
<pre>
    <code>
    $ mkdir git-from-dockerfile
    $ cd git-from-dockerfile
    </code>
</pre>

<hr>
<p>작성중...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] OpenCV SystemError : May 17, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-OpenCV-SystemError-May-17-2021</link>
            <guid>https://velog.io/@re_brother/TIL-OpenCV-SystemError-May-17-2021</guid>
            <pubDate>Mon, 17 May 2021 03:51:22 GMT</pubDate>
            <description><![CDATA[<h2 id="opencv-systemerror">OpenCV SystemError</h2>
<h3 id="built-in-function-imwrite-returned-null-without-setting-an-error">built-in function imwrite returned NULL without setting an error.</h3>
<blockquote>
<p>OpenCV를 이용하여 jpg to png 변환 Process 과정을 거칠 상황이 생겨 코드를 작성하던 중 마주친 에러</p>
</blockquote>
<pre><code class="language-python">def jpg_to_png(path):
    filePath = &#39;{}&#39;.format(path)
    directory_name = basename(path)
    print(&#39;target directory :&#39;, filePath)
    fileExt = r&#39;*.jpg&#39;
    target_list = list(pathlib.Path(filePath).glob(fileExt))

    for target in range(0, len(target_list)):
        print(&#39;[INFO] target_list[{}] :&#39;.format(target), target_list[target])
        target_file = target_list[target]
        img = cv2.imread(target_file)
        cv2.imwrite(r&#39;{0}\{1}_{2}.png&#39;.format(filePath, directory_name, target), img)
        print(&#39;[COMPLETE] {0} / {1} :&#39;.format(target, len(target_list)))</code></pre>
<p><code>argparse</code>를 통해 변환 디렉토리에 대한 path를 인자로 받아 <code>path</code> 변수로 해당 경로를 전달하여 디렉토리 내에 jpg 파일을 png로 변환하는 코드이다.</p>
<pre><code class="language-python">img = cv2.imread(target_file)</code></pre>
<p>해당 부분에서 OpenCV SystemError가 발생했고, 해당 오류 수정을 위해 인터프리터 환경으로 전환하여 오류 확인을 진행하였다.
<img src="https://images.velog.io/images/re_brother/post/373c886e-6c56-450a-9687-0432ff52f9cd/image.png" alt="">일반적으로 파일의 경로는 <code>str</code> 타입으로 받아오는 경우가 많은데, 디렉토리 내에 jpg 파일 리스트를 가져오는 과정에서 사용한 <code>pathlib</code> 패키지에서 <code>WindowsPath</code> 타입으로 경로를 받아오는 현상이 발생했다.</p>
<p>의외로 타입 변환을 통해 간단하게 해결했다.</p>
<pre><code class="language-python">
img = cv2.imread(str(target_file))</code></pre>
<h3 id="결론">결론</h3>
<p>쉽게 해결할 수 있는 문제는 차근차근 쉽게 생각해서 쉽게 해결해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] tmux Command : February 17, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-tmux-Command-February-17-2021</link>
            <guid>https://velog.io/@re_brother/TIL-tmux-Command-February-17-2021</guid>
            <pubDate>Wed, 17 Feb 2021 09:02:37 GMT</pubDate>
            <description><![CDATA[<h1 id="tmux">tmux</h1>
<p><code>ubuntu</code> 시스템에서 종종 사용하는 <code>tmux</code>인데, 자주 사용하는 유틸리티가 아니다보니 종종 사용법을 잊는 경우가 발생하여 벨로그에 남겨봐야겠다.</p>
<h2 id="tmux-installubuntu">tmux install(ubuntu)</h2>
<p>다른 시스템에선 사용해본적이 없어서 어떻게 설치하는지는 모르겠다. 아래는 Ubuntu 기준으로 작성됨.</p>
<pre><code class="language-bash">$ apt-get install tmux</code></pre>
<h2 id="tmux-command">tmux command</h2>
<p><code>tmux</code>가 다 좋은데 커맨드가 머리에 잘 안남는다. 내가 멍청한 이유일지도 모르겠다.
아무튼 <code>tmux</code>의 커맨드는 <code>ctrl+b</code>가 기본이며, 이후 추가 입력키를 입력해서 사용하는 방식이다.</p>
<h3 id="tmux-session">tmux Session</h3>
<h4 id="create-session">create session</h4>
<pre><code class="language-bash">$ tmux new -s [name]</code></pre>
<h4 id="kill-session">kill session</h4>
<pre><code class="language-bash">$ tmux kill-session -t [name]</code></pre>
<h4 id="hide-session">hide session</h4>
<p>새롭게 생성한 세션에서 할 일을 마친 뒤 유지시키고 싶은 상태에서 기존 터미널로 나가고 싶은 경우</p>
<pre><code class="language-bash">$ ctrl + b + d</code></pre>
<h4 id="visible-session">visible session</h4>
<p><code>hide session</code>을 통해 벗어난 세션에 복귀하고 싶은 경우</p>
<pre><code class="language-bash">$ tmux a -t [name]</code></pre>
<h3 id="tmux-window">tmux window</h3>
<p><code>window</code>는 탭 정도라고 생각하면 됨.</p>
<h4 id="create-window">create window</h4>
<pre><code class="language-bash">$ ctrl + b + c</code></pre>
<h4 id="move-window">move window</h4>
<pre><code class="language-bash">$ ctrl + b + [window number]</code></pre>
<h3 id="panes">panes</h3>
<p><code>panes</code>는 화면 분할(split)에 사용된다.</p>
<h4 id="horizontal-split">horizontal split</h4>
<pre><code class="language-bash">$ ctrl + b + %</code></pre>
<h4 id="vertical-split">vertical split</h4>
<pre><code class="language-bash">$ ctrl + b + &quot;</code></pre>
<h4 id="panes-move">panes move</h4>
<p><code>panes</code>간에 커서를 이동할 때 사용</p>
<pre><code class="language-bash">$ ctrl + b + [arrow]</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Flutter Study 4 : February 9, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-Flutter-Study-4-January-13-2021</link>
            <guid>https://velog.io/@re_brother/TIL-Flutter-Study-4-January-13-2021</guid>
            <pubDate>Tue, 09 Feb 2021 10:47:23 GMT</pubDate>
            <description><![CDATA[<h2 id="flutter-study-list">Flutter Study List</h2>
<h3 id="1-text-gradient--scroll-list">1. Text Gradient / Scroll list</h3>
<h3 id="2-pushnamedandremoveuntil">2. pushNamedAndRemoveUntil</h3>
<h3 id="3-alertdialog">3. AlertDialog</h3>
<h3 id="4-automatickeepaliveclientmixin">4. AutomaticKeepAliveClientMixin</h3>
<h3 id="5-flutter-youtube">5. Flutter Youtube</h3>
<p><a href="https://www.youtube.com/channel/UCqxo_5t5-_Uhq9TfhTAat0A/videos">헤비프랜 - Heavy Fran Youtube</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Node.js System Structure : January 21, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-Node.js-System-Structure-January-21-2021</link>
            <guid>https://velog.io/@re_brother/TIL-Node.js-System-Structure-January-21-2021</guid>
            <pubDate>Thu, 21 Jan 2021 11:01:03 GMT</pubDate>
            <description><![CDATA[<h2 id="nodejs-system-structure">Node.js System Structure</h2>
<p><img src="https://images.velog.io/images/re_brother/post/b05de7e8-bccb-4f6f-91b7-73216a012028/image.png" alt=""></p>
<h3 id="concurrency--throughput">Concurrency / Throughput</h3>
<p>Node.js에서 JavaScript 실행이 싱글 스레드(이벤트 루프와 같은 스레드)에서 동작합니다. 따라서 Node.js의 동시성은 다른 작업이 완료된 후에 JavaScript 콜백 함수를 실행하는 이벤트 루프의 능력을 의미한다.
JavaScript 실행은 하나의 이벤트 루프에서만 동작하지만 이벤트 루프의 여러 콜백 함수를 실행하여 동시에 처리되도록 보이는 것을 의미한다.</p>
<p>동시에 실행되어야 하는 모든 코드는 I/O 등의 JavaScript가 아닌 작업(이벤트 루프 스레드 외 다른 스레드)이 일어나는 동안 이벤트 루프가 계속 실행될 수 있도록 해야한다.</p>
<p>이벤트 루프는 동시 작업을 다루려고 부가적인 스레드를 만드는 다른 언어의 모델과는 다르다. (<code>Node.js</code>는 스레드를 따로 생성하지 않음) =&gt; (<code>Node.js</code> 10버전으로 들어오면서 <code>Worker_thread</code>라는 모듈을 통해 스레드 생성 가능하도록 변경됐다.)</p>
<h3 id="nodejs-system">Node.js System</h3>
<p><img src="https://images.velog.io/images/re_brother/post/68f2e2e4-7778-42fe-babb-a9fc0659cf82/image.png" alt="">
<code>libuv</code>란 비동기 I/O에 집중하는 멀티 플랫폼 라이브러리이다. C언어로 개발되었으며 사실상 <code>Node.js</code>를 위해 개발된 것이다. 이 라이브러리는 다양한 I/O 폴링 메커니즘에 대한 단순한 추상화 이상의 기능을 제공한다. 어느 운영체제에서도 가능한 <code>file I/O</code>와 <code>thread</code> 기능 또한 제공된다. </p>
<p><code>libuv</code>에는 <code>thread pool</code>이 존재하는데 이 <code>thread pool</code>에 있는 <code>thread</code>가 동기적인 입출력 작업을 이벤트 루프 대신 처리를 해준다. 또한 이 <code>libuv</code>를 통해 이벤트 루프를 제어한다.</p>
<h3 id="event-loop">Event Loop</h3>
<p>이벤트 루프는 가능하다면 언제나 시스템 커널에 작업을 떠넘겨서 Node.js가 논블로킹 I/O 작업을 수행하도록 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Flutter Study 3 : January 12, 2021]]></title>
            <link>https://velog.io/@re_brother/TIL-Flutter-Study-3-January-12-2021</link>
            <guid>https://velog.io/@re_brother/TIL-Flutter-Study-3-January-12-2021</guid>
            <pubDate>Tue, 12 Jan 2021 08:23:30 GMT</pubDate>
            <description><![CDATA[<h2 id="flutter-app-launcher-icon">Flutter App launcher icon</h2>
<h3 id="1-library">1. Library</h3>
<p>기본적으로 <code>Flutter</code> 개발 중 아이콘 변경을 진행은 가능하지만 그 과정이 어느정도의 귀찮음을 동반하기 때문에 외부 라이브러리를 활용하여 간편하게 App Launcher Icon 변경하는 방법에 대해 작성해볼까 한다.</p>
<h4 id="flutter_launcher_icons">flutter_launcher_icons</h4>
<p><img src="https://images.velog.io/images/re_brother/post/23b3f1b4-29c6-4875-a09d-52b408a1add4/image.png" alt="">
<code>flutter_launcher_icons</code> : 기존에 다소 복잡했던 아이콘 변경의 과정을 커맨드 라인으로 간편하게 아이콘 변경이 가능한 라이브러리. 아이콘 롤백이 필요한 경우 이전 런처 아이콘을 유지할 수있는 옵션 존재.
작성일 기준 2020년 8월에 발표된 <code>flutter_launcher_icons 0.8.1</code>이 LTS 버전이다.</p>
<p>라이브러리 설치는 <code>pubspec.yaml</code> 하위에 있는 <code>dev_dependencies</code>에 아래와 같이 추가하면 된다.</p>
<pre><code class="language-yaml">dev_dependencies:
        ...
        ...
        ...
    flutter_launcher_icons: ^0.8.1</code></pre>
<blockquote>
<p>외부 라이브러리 설치에 대한 설명은 아래 링크
<a href="https://velog.io/@re_brother/TIL-Flutter-Study-2-December-23-2020">https://velog.io/@re_brother/TIL-Flutter-Study-2-December-23-2020</a>
<strong>4. Client-Server Test using Node.js - Export Module</strong> 항목에 작성되어있다.</p>
</blockquote>
<h3 id="2-configure">2. Configure</h3>
<h4 id="icon">icon</h4>
<p>아이콘은 1024px * 1024px, Alpha(투명) 값이 없어야 한다.
해당 아이콘을 <code>./assets/</code> 디렉터리에 추가하고, <code>pubspec.yaml &gt; assets</code>에 <code>path</code>를 추가한다.</p>
<h4 id="flutter_launcher_iconsyaml">flutter_launcher_icons.yaml</h4>
<p><code>pubspec.yaml</code>과 동일한 디렉터리에 <code>flutter_launcher_icons.yaml</code>파일을 생성한 뒤 아래와 같이 작성한다.</p>
<pre><code class="language-yaml">flutter_icons:
  android: &quot;launcher_icon&quot;
  ios: true
  image_path: &quot;assets/icon.png&quot;</code></pre>
<p><code>image_path</code>는 실제 아이콘으로 사용할 이미지의 <code>path</code>를 입력한다.</p>
<h3 id="3-command-run">3. Command run</h3>
<p>터미널에 <code>flutter pub run flutter_launcher_icons:main</code> 입력 후 run
<img src="https://images.velog.io/images/re_brother/post/e45b810c-3014-46ab-be8d-294bb3ec0f91/image.png" alt="">
위와 같은 콘솔이 출력되었다면 정상적으로 아이콘이 변경된 것을 확인할 수 있다.</p>
<h2 id="gradle-threw-an-error-while-downloading-artifacts-from-the-network">Gradle threw an error while downloading artifacts from the network</h2>
<p><code>Flutter App</code> 테스트를 진행하던 중 <code>[!] Gradle threw an error while downloading artifacts from the network</code> 에러가 발생했다. 딱히 작업 중에 다른 파일을 수정했던건 없었는데 무슨 이유인지 앱 빌드가 되지 않아 나중을 위해 해결 방법을 작성해놓을까 한다.</p>
<h3 id="gradle-wrapperjar">gradle-wrapper.jar</h3>
<p>&gt; <code>.\flutter_app\android\gradle</code> 위치에 있는 <code>gradle-wrapper.jar</code>파일 삭제</p>
<h3 id="gradle-wrapperproperties">gradle-wrapper.properties</h3>
<ol>
<li><code>gradle-wrapper.jar</code>와 동일 디렉터리에 있는 <code>gradle-wrapper.properties</code> 파일 Open</li>
<li><pre><code class="language-c">#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-5.6.2-all.zip```

</code></pre>
</li>
</ol>
<p><code>gradle-wrapper.properties</code> 파일 하단에 존재하는 <code>distributionUrl=https://.../gradle-5.6.2-all.zip</code>을 <code>https://.../gradle-6.4.1-all.zip</code>으로 변경한 뒤에 터미널에 <code>flutter run</code> 입력 후 정상적으로 컴파일이 진행되는지 확인한다.</p>
<h3 id="another-solution">Another Solution</h3>
<p>위 방법이 아닌 터미널창에 <code>flutter build</code> 명령어를 입력하면 해결된다는 글이 있는데, 해결 가능한 방법인지 아닌지는 확인을 하지 못했다. 아무튼 그런 방법도 있다고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Flutter Study 2 : December 23, 2020]]></title>
            <link>https://velog.io/@re_brother/TIL-Flutter-Study-2-December-23-2020</link>
            <guid>https://velog.io/@re_brother/TIL-Flutter-Study-2-December-23-2020</guid>
            <pubDate>Wed, 23 Dec 2020 09:04:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/re_brother/post/2b756bf6-9ba9-4256-86d7-c5012e1b83b0/image.png" alt="">
<a href="https://velog.io/@re_brother/TIL-Flutter-Study-1-December-16-2020">Flutter Study 1</a>에서는 기본적인 <code>Flutter</code>의 기본적인 설명과 Install에 대한 내용을 다루었다.
이번엔 총 4가지 테스트를 진행해볼까 한다.</p>
<blockquote>
<p><strong>1. Multiple FloatingActionButton</strong>
<strong>2. Image Move</strong>
<strong>3. Button Label</strong>
<strong>4. Client-Server Test using Node.js</strong></p>
</blockquote>
<h3 id="1-multiple-floatingactionbutton">1. Multiple FloatingActionButton</h3>
<p>일반적인 <code>Flutter</code>개발 환경에서는 <code>FloatingActionButton</code>을 1개만 선언이 가능하다. 하지만 <code>Stack</code> 등을 이용하여 다중의 <code>FloatingActionButton</code>을 구현 가능하며, 테스트를 진행하던 도중 <code>Scaffold</code> 구조로 작성된 코드의 경우 정상적으로 구동이 불가능한 현상이 발생했다. 물론 <code>FloatingActionButton</code> Asset 외에도 <code>RaisedButton</code>, <code>FlatButton</code>, <code>IconButton</code> 등이 존재하지만 막연하게
&quot;<strong>다중 <code>FloatingActionButton</code>을 만들 수 있는 방법은 없을까?</strong>&quot; 라는 생각이 들었다.</p>
<h4 id="what-happened">What happened?</h4>
<p>엄청난 일이 있었던건 아니다. 앞에 말했듯이 막연한 생각으로 <code>FloatingActionButton</code>을 하나 더 만들어보고 싶었고, <code>Flutter</code>에서는 <code>FloatingActionButton</code>은 더이상 선언이 불가능하다. </p>
<h4 id="try-1">Try 1</h4>
<p>물론 아예 방법이 없진 않았다. 아래와 같이 <code>FloatingActionButton</code>을 <code>Stack</code>으로 묶어 <code>Align</code>으로 나눈 다음 선언을 한다면 한 화면 내에 여러 개의 <code>FloatingActionButton</code> 생성이 가능하다.</p>
<pre><code class="language-dart">Stack(
  children: &lt;Widget&gt;[
    Align(
      alignment: Alignment.bottomLeft,
      child: FloatingActionButton(...),
    ),
    Align(
      alignment: Alignment.bottomRight,
      child: FloatingActionButton(...),
    ),
  ],
)</code></pre>
<h4 id="try-2">Try 2</h4>
<p>현재 코드는 <code>Scaffold</code> 구조로 구성되어 있어서, 아래와 같이 <code>Stack</code> 앞에 <code>floatingActionButton</code>을 선언한다.</p>
<pre><code class="language-dart">floatingActionButton: Stack(
  children: &lt;Widget&gt;[
    Align(
      alignment: Alignment(Alignment.bottomRight.x, Alignment.bottomRight.y - 0.4),
      child: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: &#39;Increment&#39;,
        child: Icon(Icons.arrow_upward_sharp),
      ),
    ),
    Align(
      alignment: Alignment(Alignment.bottomRight.x, Alignment.bottomRight.y - 0.2),
      child: FloatingActionButton(
        onPressed: _decrementCounter,
        tooltip: &#39;Decrement&#39;,
        child: Icon(Icons.arrow_downward_sharp),
      ),
    ),
    Align(
      alignment: Alignment.bottomRight,
      child: FloatingActionButton(
        onPressed: _initCounter,
        tooltip: &#39;Initialize&#39;,
        child: Icon(Icons.album_outlined),
      )
    )
  ],
),</code></pre>
<h4 id="result">Result</h4>
<p><img src="https://images.velog.io/images/re_brother/post/83cb41d4-730c-4a16-bd9b-45ad06180716/image.png" alt="">
<code>Alignment</code>의 <code>y</code>값을 받아 각 버튼의 위치를 <code>0.2</code>, <code>0.4</code>씩 변경했다.</p>
<h3 id="2-image-move">2. Image Move</h3>
<p><code>Multiple FloatingActionButton</code>에서 여러 개의 <code>FloatingActionButton</code>을 통해 <code>counter</code> 변수의 값을 증가/감소할 수 있는 기능을 구현했다.
해당 기능을 활용하여 import된 이미지의 위치를 조정하는 기능을 구현해보자.</p>
<pre><code class="language-dart">    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            Container(
              //Image.asset(&#39;assets/images/test_img.png&#39;),
              color: Colors.blue,
              width: 200,
              height: 200,
            ),

            Text(
              &#39;You have pushed the button this many times:&#39;,
            ),
            Text(
              &#39;$_counter&#39;,
              style: Theme.of(context).textTheme.headline2,
            ),
          ],
        ),
      ),</code></pre>
<p>이미지를 삽입하기 전 <code>body</code>에 테스트용 <code>Container</code>를 추가하여 해당 영역 안에 이미지를 배치할 예정이다.</p>
<h4 id="import-image">Import Image</h4>
<p><img src="https://images.velog.io/images/re_brother/post/608cdc98-364d-41e8-a0a6-c9549f085cfa/image.png" alt="">
현재 프로젝트 디렉터리 구조이다. 해당 프로젝트에서 사용되는 외부 이미지와 리소스들은 모두 <code>pubspec.yaml</code>에 path를 꼭 작성해야한다.</p>
<pre><code class="language-yaml">flutter:
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  assets:
    - assets/images/test_img.png
    ...</code></pre>
<p>물론 1개의 파일만 추가한다면 해당 파일의 상대 경로(assets/images/test_img.png)를 추가하면 된다. 하지만 하나의 파일이 아닌 디렉터리 단위로 추가하게 될 경우 해당 디렉터리의 상대 경로(assets/images/)를 추가하면 된다.
이 때 상대 경로의 기준점은 <code>pubspec.yaml</code>이다.</p>
<p>이렇게 외부 이미지 경로에 대한 정의를 한 뒤에 <code>main.dart</code> 파일로 돌아와서 해당 이미지를 출력한다.</p>
<pre><code class="language-dart">return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            Container(
              color: Colors.blue,
              width: 200,
              height: 200,
            ),

            Image.asset(&#39;assets/images/test_img.png&#39;),

            Text(
              &#39;You have pushed the button this many times:&#39;,
            ),
            Text(
              &#39;$_counter&#39;,
              style: Theme.of(context).textTheme.headline2,
            ),
          ],
        ),
      ),</code></pre>
<p><code>Image.asset()</code>은 로컬에 위치한 이미지를 출력할때 사용되고 네트워크에 위치한 이미지를 Load 해야하는 경우에는 <code>Image.network()</code>를 사용한다.</p>
<h4 id="image-location">Image Location</h4>
<p>현재 테스트 프로젝트는 <code>mainAxisAlignment</code>로 각 <code>material</code>들의 <code>y</code>축이 상대적으로 고정되어있기 때문에 이미지를 <code>Container</code>에 배치한 상태로 이미지의 <code>x-Alignment Value</code>를 변경할 예정이다.</p>
<pre><code class="language-dart"> return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            Container(
              child: Image.asset(&#39;assets/images/test_img.png&#39;, width:200, height:200, fit: BoxFit.fill),
              padding: EdgeInsets.only(left: 2, right: 2, bottom: 1),
              alignment: Alignment(0 - (100 - _counter)/100, 0),
            ),

            // Image.asset(&#39;assets/images/test_img.png&#39;),

            Text(
              &#39;You have pushed the button this many times:&#39;,
            ),
            Text(
              &#39;$_counter&#39;,
              style: Theme.of(context).textTheme.headline2,
            ),
          ],
        ),
      ),</code></pre>
<p>위와 같이 코드를 작성하였으며, 이미지 <code>Container</code>의 <code>x</code>좌표의 위치를 변경하기 위해 해당 컨테이너의 <code>alignment</code>를 <code>Alignment(0 - (100 - _counter)/100, 0)</code>로 선언하여 <code>_counter</code>의 값이 변경될 때 마다 이미지의 위치가 변경된다.
<img src="https://images.velog.io/images/re_brother/post/4e98eb8d-266f-42d8-b2df-0376e50b2ab0/Demo_flutter.gif" alt=""></p>
<h3 id="3-button-label">3. Button Label</h3>
<p><code>Fluter Official Documents</code>를 보다가 각 버튼에 <code>label</code>이 가능하다는 사실을 알게되었고 이 <code>label</code> 기능을 추가해볼까 한다.
<img src="https://images.velog.io/images/re_brother/post/72acc462-c3e5-4522-accd-ee69a4019234/image.png" alt="">
위 이미지 처럼 기존에 정의해놓은 아이콘 우측으로 <code>label</code>을 추가하는 방식이다.</p>
<pre><code class="language-dart">      // Origin Code
      floatingActionButton: Stack(
        children: &lt;Widget&gt;[
          Align(
            alignment: Alignment(Alignment.bottomRight.x, Alignment.bottomRight.y - 0.4),
            child: FloatingActionButton(
              onPressed: _incrementCounter,
              tooltip: &#39;Increment&#39;,
              child: Icon(Icons.arrow_upward_sharp),
              backgroundColor: Colors.indigo,
            ),
          ),</code></pre>
<p>기존 <code>FloatingActionButton</code>에 바로 <code>label</code>속성을 추가하게 되면 <code>child</code>에 종속되어있는 <code>Icon</code>속성과 정상적으로 매칭되지 않아 아래와 같은 오류가 발생한다.
<img src="https://images.velog.io/images/re_brother/post/2a5d5152-fab0-4e1b-abac-f13313e876a7/image.png" alt="">
일반적인 버튼에 <code>Label</code>속성을 추가하기 위해서는 <code>FloatingActionButton</code>의 <code>extended</code> 메소드를 활용해야한다.</p>
<pre><code class="language-dart">      floatingActionButton: Stack(
        children: &lt;Widget&gt;[
          Align(
            alignment: Alignment(Alignment.bottomRight.x, Alignment.bottomRight.y - 0.4),
            child: FloatingActionButton.extended(
              onPressed: _incrementCounter,
              tooltip: &#39;Increment&#39;,
              icon: Icon(Icons.arrow_upward_sharp),
              label: Text(&#39;Increment&#39;),
              backgroundColor: Colors.indigo,
            ),
          ),</code></pre>
<p><code>FloatingActionButton.extended</code> 하위에 <code>icon</code>과 <code>label</code>을 새롭게 선언하여 조금 더 직관적인 UI를 만들 수 있다.
<img src="https://images.velog.io/images/re_brother/post/b7cbf9fd-e3a1-46d5-aefa-9decea1106ee/image.png" alt=""></p>
<h3 id="4-client-server-test-using-nodejs">4. Client-Server Test using Node.js</h3>
<p>간단하게 <code>Flutter</code> 앱 내에서 <code>Node.js</code> 서버와 통신을 진행해보려 한다.
일단 <code>Node.js</code> 서버는 요청에 대한 응답만 넘겨주는 정도로 간단하게 구현한다.</p>
<h4 id="servernodejs">Server(Node.js)</h4>
<pre><code class="language-js">
const http = require(&quot;http&quot;);

let fs = require(&#39;fs&#39;);
let bodyParser = require(&#39;body-parser&#39;);
// DB Start
const sqlite3 = require(&#39;sqlite3&#39;).verbose();
let sql = `SELECT * FROM user`;

let db = new sqlite3.Database(&#39;./app/database.db&#39;, (err) =&gt; {
        if (err) {
                return console.error(err.message);
        }

        console.log(&#39;Connected to the in local SQLite database.&#39;);
        db.all(sql, [], (err, rows) =&gt; {
                if (err) {
                        throw err;
                }
                rows.forEach((row) =&gt; {
                        console.log(row);
                });
        });

});
// DB End

http.createServer((req, res) =&gt; {
        res.statusCode = 200;
        res.setHeader(&#39;Content-Type&#39;, &#39;text/plain&#39;);
        res.end(&#39;Node.js Server-Client Test&#39;);
}).listen(4004, () =&gt; {
        console.log(&quot;Server Running at localhost:4004&quot;);
});</code></pre>
<p>간단하게 테스트용으로만 구현하려고 했는데, 마침 <code>SQLite</code> 테스트용 서버를 구축해놓은 코드가 있어서 해당 코드를 활용하여 테스트를 진행할 예정이다.
혹시나 DataBase가 필요없는 경우에는 주석 <code>DB Start</code>부터 <code>DB End</code>까지 삭제한 뒤에 진행하면 된다.</p>
<h4 id="export-module">Export Module</h4>
<p>물론 <code>http</code> 라이브러리가 기존에 설치되어 있다면 아무런 제약사항이 없겠지만 혹시나 <code>http</code> 모듈을 설치해야 한다면 <code>pubspec.yaml</code> -&gt; <code>dev_dependencies</code> 하위에 해당 모듈의 이름과 버전을 선언하고, 커맨드창에서 <code>flutter pub get</code> 명령어를 입력하면 자동으로 설치된다.</p>
<h4 id="clientflutter">Client(Flutter)</h4>
<p>서버와 통신하기 위해서는 <code>http</code> 라이브러리가 필요하기 때문에 <code>Flutter</code> 코드 최상단에 <code>http</code> 라이브러리를 참조한다.</p>
<pre><code class="language-dart">import &#39;package:http/http.dart&#39;;</code></pre>
<p><code>Node.js</code> Server - <code>flutter</code> Client 통신은 아래와 같이 진행하였으며, 이에 대한 내용은 주석으로 번호를 표시하여 설명하겠다.</p>
<pre><code class="language-dart">  @override
  _MyHomePageState createState() =&gt; _MyHomePageState();
}

class _MyHomePageState extends State&lt;MyHomePage&gt; {
  // Section 1
  String serverResponse = &#39;&#39;;

  // Section 2
  _makeGetRequest() async {
    Response response = await get( _nodeCall() );
    setState(() {
      serverResponse = response.body;
    });
  }

  // Section 3
  String _nodeCall()
  {
      return &#39;http://10.110.31.211:4004&#39;;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            // Section 4
            Text(&#39;Server Response\n&#39;, style: Theme.of(context).textTheme.headline5),
            Text(&#39;[ &#39; + serverResponse + &#39; ]&#39;, style: Theme.of(context).textTheme.headline6),
            Container(
              child: Image.asset(&#39;assets/images/test_img.png&#39;, width:200, height:200, fit: BoxFit.fill),
              padding: EdgeInsets.only(left: 2, right: 2, bottom: 1),
              alignment: Alignment(0 - (100 - _counter)/100, 0),
            ),

            Text(
              &#39;$_counter&#39;,
              style: Theme.of(context).textTheme.headline2,
            ),
          ],
        ),
      ),

      floatingActionButton: Stack(
        children: &lt;Widget&gt;[
          Align(
            alignment: Alignment.bottomRight,
            child: FloatingActionButton.extended(
              // Section 5
              onPressed: _makeGetRequest,
              tooltip: &#39;Initialize&#39;,
              icon: Icon(Icons.album_outlined),
              label: Text(&#39;  Node.js   &#39;),
              backgroundColor: Colors.blue,
            )
          )
        ],
      ),
    );</code></pre>
<blockquote>
<p><strong>Section 1</strong> : <code>String</code> 형태의 <code>serverResponse</code>를 선언한다. 해당 변수는 최초 <code>init</code>시에 공백으로 보여지게 되며, 이는 <code>Section 2</code>에서 클라이언트 요청에 대한 서버의 <code>response</code>를 표시한다.
<strong>Section 2</strong> : <code>async</code> 방식으로 서버에 요청한다. <code>Section 3</code>에서 요청된 응답을 <code>get</code>하여 <code>response</code>에 저장한다.
<strong>Section 3</strong> : 지정해놓은 서버에 요청을 전달하여, 응답 내용을 <code>return</code>한다.
<strong>Section 4</strong> : 응답받은 <code>serverResponse</code>를 출력한다.
<strong>Section 5</strong> : <code>FloatingActionButton</code>에 <code>_makeGetRequest Function</code>을 적용한다.</p>
</blockquote>
<h3 id="5-reference-pages">5. Reference Pages</h3>
<p>구글링을 통해 여러 문제들을 해결했지만, 정리해놓고 필요할 때 마다 참고하면 좋을 블로그들</p>
<blockquote>
<p><a href="https://kyungsnim.tistory.com/74"> ▶ Flutter Widget 정리 Tistory</a>
<a href="https://pythonkim.tistory.com/110"> ▶ Flutter Image Properties 정리 Tistory</a>
<a href="https://velog.io/@wonhs717/%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4Markdown-%EB%AC%B8%EB%B2%95-ytk5zemk0x"> ▶ Markdown 문법 정리 Velog</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Flutter Study 1 : December 16, 2020]]></title>
            <link>https://velog.io/@re_brother/TIL-Flutter-Study-1-December-16-2020</link>
            <guid>https://velog.io/@re_brother/TIL-Flutter-Study-1-December-16-2020</guid>
            <pubDate>Wed, 16 Dec 2020 08:31:52 GMT</pubDate>
            <description><![CDATA[<h2 id="flutter">Flutter</h2>
<h3 id="what-is-flutter">What is Flutter?</h3>
<p><img src="https://images.velog.io/images/re_brother/post/7be4d236-ebc4-4766-9918-3deb0255e675/image.png" alt="">
Flutter 는 Google 에서 만든 Cross-Platform Framework 이다.
하나의 코드 Base 로 Android / iOS 심지어 Desktop App 까지 개발 할 수 있다.
<img src="https://images.velog.io/images/re_brother/post/5a6f0f12-b513-475e-80a0-b09b5c0d08d4/image.png" alt="">
<code>Dart</code>로 작성된 하나의 코드는 각 OS에 맞게 Convert 과정을 거쳐 Build 된다.</p>
<h3 id="flutter-development-environment">Flutter Development Environment</h3>
<h4 id="install-list">Install list</h4>
<ol>
<li>Android Studio
<a href="https://developer.android.com/studio">[Link]</a></li>
<li>Android Plug-in
<code>Android Studio</code> 실행 후 <code>[Files] -&gt; [Settings]</code> 또는 <code>Ctrl + Alt + S</code> 단축키를 이용하여 <code>Plugins</code> 탭에서 <code>Flutter</code>를 설치한다.
<img src="https://images.velog.io/images/re_brother/post/974dc695-9dc1-475b-8767-c7a0679b0f5a/image.png" alt="">
<code>Flutter</code> Plugin을 설치하게 되면 <code>Dart</code> Plugin도 함께 설치된다.</li>
<li>Android Virtual-Machine
<code>[Tools] -&gt; [AVD Manager]</code> 또는 우측 상단 아이콘을 클릭하여 <code>AVD Manager</code>를 실행한다.
<img src="https://images.velog.io/images/re_brother/post/a77c9827-108f-460d-a1b4-7a2b6c1a7fbf/image.png" alt="">
<img src="https://images.velog.io/images/re_brother/post/a562a745-767c-4010-8e8e-046aed70ecaa/image.png" alt=""></li>
</ol>
<p><code>Create Virtual Device</code> 버튼을 클릭해서 새로운 안드로이드 가상 머신을 생성한다.
Android 버전은 자유롭게 선택해도 되지만, 호환 및 테스팅 이슈가 존재할 수 있으므로 Android 10 또는 11을 사용하기를 추천한다.</p>
<ol start="4">
<li>Flutter SDK
<a href="https://flutter-ko.dev/docs/get-started/install/windows">[Link]</a>
<code>Flutter SDK</code>를 다운받아 압축 해제 후 <code>flutter</code> 폴더 전체를 C드라이브 최상위 디렉터리로 이동한다.</li>
</ol>
<p><img src="https://images.velog.io/images/re_brother/post/109b96a6-7ce9-4022-83bd-d2b73fddcdfd/image.png" alt=""></p>
<p>정상적으로 폴더를 이동했다면 <code>C:\flutter\</code>에는 위와 같은 structure를 가진다.
이후 <code>SystemPropertiesAdvanced</code>를 실행하여 <code>C:\flutter\bin\</code> 경로를 환경변수 <code>PATH</code>에 추가한다.
환경변수 추가까지 정상적으로 진행했다면 명령 프롬프트(cmd)에 <code>flutter doctor</code>를 실행해본다.</p>
<p><img src="https://images.velog.io/images/re_brother/post/bad59af3-6d02-4425-9467-7a536297ba57/image.png" alt=""></p>
<p>최초 실행 시에는 위와 같은 화면이 출력된다.
<img src="https://images.velog.io/images/re_brother/post/4c9db769-4e2f-4702-8c5d-59e01d5d1705/image.png" alt=""></p>
<p>이어서 위와 유사한 커맨드가 나오게 되는데, 아마 <code>Android toolchain</code> 부분에 <code>WARNING</code>이 존재한다.
안드로이드 라이센스 관련된 내용인데 해당 부분에 대한 해결을 위해 커맨드에 <code>flutter doctor --android-licenses</code>를 입력하고,
<code>accepted (y/N)?</code> 에서 전부 <code>y</code>를 해주면 된다.</p>
<p>여기까지 진행이 완료되었다면 <code>Android Studio</code>, <code>VS Code</code>, <code>Connected device</code> 총 3개의 <code>warning</code>이 존재하는데 <code>Android Studio Warning</code>은 실제 개발 환경에 추가 Plugin을 설치했기 때문에 패스해도 된다고 한다.
<code>VS Code</code>는 개발 환경에 추가하지 않을 예정이기 때문에 패스할 것이고, <code>Connected device</code>는 현 시점에서 <code>Virtual-Machine</code>이 동작하지 않고 있기 때문에 발생하는 오류이다.</p>
<h4 id="tip">TIP?..</h4>
<p>혹시나 커맨드창에 <code>flutter doctor</code> 명령어가 정상적으로 실행되지 않고 <strong>&#39;flutter&#39;는 내/외부 명령, 실행 프로그램, 배치 파일이 아니라고</strong> 뜨는 경우에는 환경 변수가 정상적으로 등록되지 않은 경우이므로, 커맨드창에 <code>set</code> 명령어를 입력하여 환경변수를 시스템에 적용하면 된다.</p>
<h3 id="create-android-studio-flutter-project">Create Android Studio Flutter Project</h3>
<h4 id="android-studio-실행-시-관리자-권한으로-실행해주세요">Android Studio 실행 시 관리자 권한으로 실행해주세요.</h4>
<p><code>Android Studio</code>를 <strong>관리자 권한</strong>으로 실행 후 <code>[File] -&gt; [New] -&gt; [New Flutter Project]</code>를 눌러서 새 플러터 프로젝트를 생성합니다.</p>
<p><img src="https://images.velog.io/images/re_brother/post/6caa8ed3-f350-474f-8962-1c2ec45deede/image.png" alt=""></p>
<p>템플릿의 경우 <code>Flutter Application</code>을 선택하고, 다음 페이지로 넘어갑니다.
<img src="https://images.velog.io/images/re_brother/post/047ceb0f-39ec-4333-ba10-c08b922cab55/image.png" alt=""></p>
<p><code>Project Name</code>, <code>Description</code>은 자유롭게 작성하고 <code>Flutter SDK path</code>에 아까 다운로드받은 <code>Flutter SDK</code>의 <strong>절대경로</strong>를 입력한다.
제대로 안적으면 다음으로 넘어갈 수 없으니 꼭 유의해서 작성하도록 한다.
<img src="https://images.velog.io/images/re_brother/post/283abbce-c86a-4158-a0c8-f744044d1a92/image.png" alt=""></p>
<p>Android 개발만 진행한다면 <code>Include Kotlin support for Android code</code>만 체크하면 되고, 반대로  iOS 개발만 진행한다면 <code>Include Swift support for iOS code</code>만 체크하면 된다.
굳이 하나의 플랫폼만 개발할 이유는 없으므로 양쪽 다 체크하고 프로젝트 생성을 마친다.</p>
<p><img src="https://images.velog.io/images/re_brother/post/21325639-bba1-4762-9430-11cb734c6f09/image.png" alt=""></p>
<p>프로젝트 생성이 완료되면 새로운 <code>Android Studio</code>가 열리고, 좌측에 아래와 같은 파일 및 폴더들이 생성된 것을 확인할 수 있다.
<img src="https://images.velog.io/images/re_brother/post/0042da21-d513-426b-a7d9-d39552fd7fbf/image.png" alt=""></p>
<p>해당 폴더와 파일이 무엇을 의미하는지 간략하게 설명하고 마무리할까 한다.</p>
<blockquote>
</blockquote>
<p><code>android</code> – Android Application을 만들기 위해 자동으로 생성된 소스코드
<code>ios</code> – iOS Application을 만들기 위해 자동으로 생성된 소스코드
<code>lib</code> – <code>Flutter Framework</code>를 사용하여 쓰여진 다트 코드를 담고 있는 메인 폴더
<code>lib/main.dart</code> – <code>Flutter Application</code>의 엔트리 포인트
<code>test</code> – <code>Flutter Application</code>을 테스트하기 위한 <code>Dart</code> 코드를 담고 있는 폴더
<code>test/widget_test.dart</code> – Sample Code
<code>.gitignore</code> – Git에 들어가지 않을 파일들 설정
<code>.metadata</code> – <code>Flutter</code> 툴에 의해 자동 생성됨
<code>.packages</code> – <code>Flutter</code> 패키지를 추적하기 위해 자동 생성됨
<code>.iml</code> – Android Studio에 의해 사용되는 프로젝트 파일
<code>pubspec.yaml</code> – Pub에 의해 사용되는 Flutter Package Manager</p>
]]></description>
        </item>
    </channel>
</rss>