python ORM - SQLModel - 4일차
포스트
취소

python ORM - SQLModel - 4일차

FastAPI + Next.js + PG 풀스택 nfp-boilerplate 예제를 따라하며 정리해 봅니다.

처음 계획은 FastAPI 저자가 작성한 예제 tiangolo/full-stack-fastapi-postgresql 를 따라해 보려고 했었습니다. 그러나 패키지 설치 실패, Docker 생성 실패 등의 문제가 있어서 다음으로 미루고 다른 예제를 찾아서 공부하기로 했습니다.

1
2
3
4
5
6
7
8
9
pip install cookiecutter

cookiecutter https://github.com/tiangolo/full-stack-fastapi-postgresql
# ==> 처음 project 이름만 아무거나 입력

cd { project-name }

docker-compose up -d
# ==> 실패!

0. 예제 소개

아래 모든 내용은 위 글을 기반으로 따라하며 정리하였습니다. (글쓴이에게 감사)

프로젝트 구성

  • nfp-backend
    • FastAPI
    • sqlalchemy + psycopg2
      • alembic 마이그레이션
    • postgresql
  • nfp-frontend
    • next.js
      • tailwindcss
    • pm2

브랜치 tutorial-1-how-to-build-nfp-boilerplate 다운로드

1
2
3
4
5
6
7
8
9
10
11
# 
$ git clone -b tutorial-1-how-to-build-nfp-boilerplate \
    $GITHUB/travisluong/nfp-boilerplate

$ cd nfp-boilerplate

$ ls -lt
README.md
nfp-backend
nfp-frontend

1. nfp-backend

1) poetry 패키지 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ cd nfp-backend

$ poetry init
# ----------------------------
[tool.poetry]
name = "nfp-backend"
version = "0.1.0"
description = ""
authors = ["Min Byeong-Guk <maxmin93@gmail.com>"]
readme = "README.md"
packages = [{include = "nfp_backend"}]

[tool.poetry.dependencies]
python = "^3.9"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
# ----------------------------

# create venv 
$ poetry install
$ ls -lt .venv/bin/python

$ cat requirements.txt | xargs poetry add

# installed packages list with last version
$ poetry show -l

2) database 작업

create user, database, remote access

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ ssh minubt

# createuser tonyne -P
$ createuser --interactive --pwprompt
Enter name of role to add: tonyne
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n

$ createdb -O tonyne nfp_boilerplate_dev

# SERVER: allow remote access from all
$ vi ~/Servers/agensgraph/pg_data/postgresql.conf
# ==> listen_addresses = '*'

# USER: allow remote access in local LAN with password
$ vi ~/Servers/agensgraph/pg_data/pg_hba.conf
# ==> host  all  tonyne  192.168.0.0/24  md5

$ sudo service postgresql restart
$ sudo service postgresql status

$ exit
$ psql -h minubt -U tonyne -d nfp_boilerplate_dev -W
# ==> databases list: \l
# ==> tables list: \dt
# ==> users list: \du
# ==> quit: \q

# test
> create table temp(id int primary key, name varchar(100));

참고: 나중에 user 소유로 데이터베이스를 변경하고 싶을 때

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- change owner of database
alter database nfp_boilerplate_dev owner to tonyne;

-- users: role_name, role_attr
SELECT usename AS role_name,
  CASE 
     WHEN usesuper AND usecreatedb THEN 
     CAST('superuser, create database' AS pg_catalog.text)
     WHEN usesuper THEN 
      CAST('superuser' AS pg_catalog.text)
     WHEN usecreatedb THEN 
      CAST('create database' AS pg_catalog.text)
     ELSE 
      CAST('' AS pg_catalog.text)
  END role_attributes
FROM pg_catalog.pg_user
ORDER BY role_name desc;

alembic migration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
$ poetry shell

# create init file
$ alembic init alembic  # already done

$ vi alembic.ini
# ==> sqlalchemy.url = postgresql://tonyne:tonyne@minubt/nfp_boilerplate_dev

$ alembic revision -m "create notes table"
$ ls -lt alembic/versions
7ff3d09ed552_create_notes_table.py

$ vi alembic/versions/7ff3d09ed552_create_notes_table.py
# -------------------------------

def upgrade():
    op.create_table(
        "notes",
        sa.Column("id", sa.Integer, primary_key=True),
        sa.Column("text", sa.String),
        sa.Column("completed", sa.Boolean)
    )


def downgrade():
    op.drop_table("notes")
# -------------------------------

# dry-run : check sql
$ alembic upgrade head --sql
# -------------------------------
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Generating static SQL
INFO  [alembic.runtime.migration] Will assume transactional DDL.
BEGIN;

CREATE TABLE alembic_version (
    version_num VARCHAR(32) NOT NULL,
    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
);

INFO  [alembic.runtime.migration] Running upgrade  -> 127bcea40bef, create notes table
-- Running upgrade  -> 127bcea40bef

CREATE TABLE notes (
    id SERIAL NOT NULL,
    text VARCHAR,
    completed BOOLEAN,
    PRIMARY KEY (id)
);

INSERT INTO alembic_version (version_num) VALUES ('127bcea40bef') RETURNING alembic_version.version_num;

COMMIT;
# -------------------------------

# real migration
$ alembic upgrade head
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 127bcea40bef, create notes table

# check database
$ psql -h minubt -U tonyne -d nfp_boilerplate_dev -W
# list only-tables: \dt  (시퀀스까지 모두 출력: \d)
nfp_boilerplate_dev=> \d
              List of relations
 Schema |      Name       |   Type   | Owner
--------+-----------------+----------+--------
 public | alembic_version | table    | tonyne
 public | notes           | table    | tonyne
 public | notes_id_seq    | sequence | tonyne
 public | temp            | table    | tonyne
(4 rows)

# (바로 이전) 버전 다운
$ alembic downgrade -1
# (바로 다음) 버전 업
$ alembic upgrade +1

$ exit  # from poetry shell

3) fastapi 작업

  • .env 작성
  • fastapi 실행
1
2
3
4
5
6
7
8
9
10
11
$ pwd  
{PROJECT-ROOT}/nfp-backend

# create .env
$ cat <<EOF > .env
DATABASE_URL="postgresql://tonyne:tonyne@minubt/nfp_boilerplate_dev"
>EOF

# execution with .venv 
$ poetry run uvicorn main:app --reload

API 확인

1
2
3
4
5
6
7
8
9
10
11
$ curl -X GET "http://localhost:8000/notes/"
[]%

$ curl -d '{"text":"nfp-backend tutorial", "completed":"true"}' \
-H "Content-Type: application/json" \
-X POST "http://localhost:8000/notes/"
{"id":1,"text":"nfp-backend tutorial","completed":true}%

$ curl -X GET "http://localhost:8000/notes/"
[{"id":1,"text":"nfp-backend tutorial","completed":true}]%

2. nfp-frontend

1) Next.js 셋업

React.js 의 SSR 프레임워크 (정적페이지 & 서버렌더링)

Next.js 설치

1
2
3
4
5
6
7
8
9
10
11
$ node --version
v16.17.0

$ npx --version
8.19.1

$ yarn --version
1.22.19

$ npx create-next-app --version
12.3.0

next.js examples

주의!! npx create-next-app --example 생성물과 github examples 이 다르다.

github 의 전체 examples 에서 직접 다운로드 하는 것이 올바르다!

create-next-app 의 템플릿 –typescript (–ts) 는 올바르게 나옴.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ npx create-next-app ts-starter --ts

# dependencies
- react
- react-dom
- next 

# dev-dependencies
- eslint
- eslint-config-next
- typescript
- @types/node 
- @types/react 
- @types/react-dom

$ cd ts-starter

$ yarn dev 
ready - started server on 0.0.0.0:3000, url: http://localhost:3000

이어서, next.js with tailwindcss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ yarn add -D tailwindcss postcss autoprefixer

$ npx tailwindcss init -p

# config 파일 2개 생성 (수정 사항 없음)
Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

$ vi pages/_app.tsx

# 맨 위에 추가
# -------------------------------
import 'tailwindcss/tailwind.css'
# -------------------------------

$ vi styles/globals.css

# 맨 위에 추가
# -------------------------------
@tailwind base;
@tailwind components;
@tailwind utilities;
# -------------------------------

$ yarn dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000

2) notes 페이지 추가

  • npx create-next-app nfp-frontend 생성
  • tailwindcss 적용
  • pages/notes.js 파일 생성 후, 아래 작성
  • yarn dev 확인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import Head from 'next/head'
import { useState, useEffect } from 'react';

export default function Notes() {
  const [note, setNote] = useState('');
  const [notes, setNotes] = useState([]);

  useEffect(() => {
    async function fetchNotes() {
      const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/notes/`);
      const json = await res.json();
      console.log(json)
      setNotes(json);
    }
    fetchNotes();
  }, [])

  function handleChange(e) {
    setNote(e.target.value);
  }

  async function handleSubmit() {
    const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/notes/`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        text: note,
        completed: false
      })
    })
    const json = await res.json();
    setNotes([...notes, json])
  }

  return (
    <div>
      <Head>
        <title>Notes</title>
      </Head>
      <div className="container mx-auto p-10 m-10">
        <div className="flex flex-col">
          <h1 className="font-bold mb-3">Notes</h1>
          <textarea value={note} onChange={handleChange} className="border-2" ></textarea>
          <div className="mx-auto p-3 m-5">
            <button onClick={handleSubmit} className="bg-green-500 p-3 text-white">Submit</button>
          </div>
          <div>
            <ul>

              // 완료 항목은 파란색으로, 미완료는 노란색으로 표시
              {notes && notes.map((note) =>{
                return note.completed ? (
                  <li key={note.id} className="bg-blue-100 m-3 p-3 border-blue-200 border-2">{note.text}</li>
                  ) : (
                  <li key={note.id} className="bg-yellow-100 m-3 p-3 border-yellow-200 border-2">{note.text}</li>
                  )
              })}

            </ul>
          </div>
        </div>
      </div>
    </div>
  )
}

3) NEXT_PUBLIC_API_URL 설정 (.env 파일)

  • .env.development 파일 생성
  • NEXT_PUBLIC_API_URL 작성
    • NEXT_PUBLIC_API_URL=http://localhost:8000
  • yarn dev 확인
    • Notes 에 아무말이나 작성 후, Submit 클릭!
    • 하단에 텍스트 박스 리스트로 잘 출력되면 정상
 캡쳐 - nfp-boilerplate app
<그림> 캡쳐 - nfp-boilerplate app

4) pm2 로 next app 실행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// -------------------------------
// ecosystem.config.js
// -------------------------------
'use strict';

// for pm2
module.exports = {
  apps: [
    {
      name: "nfp-frontend",  // App 이름
      script: "yarn start", 
      watch: true,           // 파일 변경시 자동 재실행
      env: {                 // 개발환경
          "NODE_ENV": "development" 
      },
      env_production: {      // 배포환경
          "NODE_ENV": "production",
          "NODE_OPTIONS": "--inspect",  // 브라우져 console 출력
          "PORT": "3000"
      }
    }
  ]
};

pm2 사용하기

  • pm2 start 로 next app 실행
  • pm2 monitor 로 모니터링 : https://pm2.io
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ pm2 start ecosystem.config.js --env production
[PM2][WARN] Applications nfp-frontend not running, starting...
[PM2] App [nfp-frontend] launched (1 instances)
┌────┬──────────────┬──────┬────┬─────────┬─────┬────────┐
│ id │ name         │ mode │ ↺  │ status  │ cpu │ memory │
├────┼──────────────┼──────┼────┼─────────┼─────┼────────┤
│ 0  │ nfp-frontend │ fork │ 0  │ online  │ 0%  │ 2.3mb  │
└────┴──────────────┴──────┴────┴─────────┴─────┴────────┘

# 리스트
$ pm2 list

# 상세 정보 (by id)
$ pm2 show 0

# 모니터링 (by name)
$ pm2 monit nfp-frontend

# app.pm2.io 으로 보기
$ pm2 monitor nfp-frontend
pm2.io - processes overview
<그림> pm2.io - processes overview
pm2.io - monitor realtime
<그림> pm2.io - monitor realtime

9. Review

  • 훗날 기술부채의 고통을 겪지 않으려면 평소에 기술 트렌드를 살펴두자
  • 관련 도구와 관련 기술이 다양해져서, 미리 셋업해 놓지 않으면 필요한 경우에 찾아 쓰기가 어렵다. 여러 기술을 하나의 패키지/튜토리얼로 묶어서 익혀두는게 좋다. 문서화도 잘 해놓자.
  • pm2 는 모니터링 외에도 버전 교체 등을 위한 reload 기능, 클러스터 설정 등이 중요한 사용 요인이다.
  • next.js 는 정적페이지 방식이지만 jekyll, gatsby 등이랑 많이 다르다. React.js 를 어느정도 숙달한 다음에 전환하는 방식이 알맞는 것 같다.
  • next.js 는 실행시 반응 속도를 보면 체감상 확실히 가볍고 빠르다.
  • tailwindcss 는 개발자 입장에서 다루기 좋은 css 로 알려져 있다.
    • 체계적으로 구성된 클래스 이름에 기능과 규칙이 있다.
    • 다양한 예제가 준비되어 있어서, 일단은 복붙으로 시작하자.

tailwindcss 참고문서

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.