Python 프로그램 웹 전환 프로젝트

PYTHON → WEB MIGRATION
Chapter 15. Python 프로그램 웹 전환 프로젝트
기존 Python/Tkinter 사원관리 프로그램을 분석하고
Vue + FastAPI 기반 웹 프로그램으로 단계적으로 전환합니다.
Python 프로그램을 웹 애플리케이션으로 전환하는 개발 과정
기존 Python 코드를 모두 버리는 것이 아니라 역할을 구분하고 재사용하는 것이 핵심입니다.
15.1 프로젝트 목표

이번 장에서는 기존 Python/Tkinter 사원관리 프로그램을 Vue + FastAPI 웹 프로그램으로 전환합니다.

기존 프로그램
employee_manager │ ├─ main.py ├─ ui.py ├─ employee_service.py ├─ employee_repository.py └─ employee.db
Tkinter → Service → Repository → SQLite
웹 전환 후
Vue
REST API
FastAPI
Service
Repository
SQLite
교체
Tkinter UI
새 화면
Vue
새로 추가되는 기술
FastAPI · REST API · Axios · Vue Router
💡 프로젝트의 핵심
기존 프로그램 전체를 처음부터 다시 만드는 것이 아닙니다. 기존 Python의 업무 로직과 DB 코드는 가능한 한 유지하고 화면과 통신 구조를 웹 방식으로 변경합니다.
15.2 기존 프로그램 확인

기존 사원관리 프로그램은 다음 기능을 가지고 있다고 가정합니다.

📋
조회
사원 목록
등록
신규 사원
✏️
수정
사원 정보
🗑️
삭제
사원 삭제
🔎
검색
이름 검색
┌─────────────────────────────────────┐ │ 사원관리 │ ├─────────────────────────────────────┤ │ 이름 [ ] │ │ 전화번호 [ ] │ │ 부서 [ 개발팀 ▼ ] │ │ │ │ [등록] [수정] [삭제] │ ├─────────────────────────────────────┤ │ 검색 [ ] [검색] │ ├─────────────────────────────────────┤ │ 번호 │ 이름 │ 전화번호 │ 부서 │ │ 1 │ 홍길동│ 010-... │ 개발팀 │ │ 2 │ 김철수│ 010-... │ 영업팀 │ └─────────────────────────────────────┘

웹에서는 기능별로 화면을 나누는 것이 편리합니다.

URL 화면
/employees사원 목록
/employees/new사원 등록
/employees/1사원 상세
/employees/1/edit사원 수정
15.3 기존 Tkinter 화면 코드
기존 Tkinter
import tkinter as tk
from tkinter import ttk


window = tk.Tk()

window.title(
    "사원관리 프로그램"
)


tk.Label(
    window,
    text="이름"
).grid(
    row=0,
    column=0
)

entry_name = tk.Entry(
    window
)

entry_name.grid(
    row=0,
    column=1
)


tk.Label(
    window,
    text="전화번호"
).grid(
    row=1,
    column=0
)

entry_phone = tk.Entry(
    window
)

entry_phone.grid(
    row=1,
    column=1
)


combo_department = ttk.Combobox(
    window,
    values=(
        "개발팀",
        "영업팀",
        "관리팀"
    )
)

combo_department.grid(
    row=2,
    column=1
)


tree = ttk.Treeview(
    window,
    columns=(
        "id",
        "name",
        "phone",
        "department"
    ),
    show="headings"
)

tree.grid(
    row=5,
    column=0,
    columnspan=2
)


window.mainloop()
웹 전환 후 사용하지 않는 화면 코드
Tk() · Label · Entry · Button · Combobox · Treeview · grid() · pack() · place() · mainloop()

이 화면 역할은 Vue가 담당합니다.

15.4 기존 Python 코드 분류하기
기존 Python 프로그램 │ ├─ UI 코드 ├─ 업무 로직 └─ DB 코드
UI 코드
Vue로 변경할 코드
entry_name.get()

tree.insert(...)

messagebox.showinfo(...)
업무 로직
계속 Python에서 사용
def validate_name(name):

    return (
        name.strip() != ""
    )
DB 코드
Repository에서 재사용
def find_all():

    cursor.execute(
        """
        SELECT *
        FROM employee
        """
    )
15.5 웹 프로젝트 구조 만들기
employee_web │ ├─ backend │ │ │ ├─ main.py │ ├─ database.py │ │ │ ├─ routers │ │ └─ employee_router.py │ │ │ ├─ services │ │ └─ employee_service.py │ │ │ ├─ repositories │ │ └─ employee_repository.py │ │ │ ├─ models │ │ └─ employee.py │ │ │ └─ employee.db │ └─ frontend │ ├─ src │ │ │ ├─ api │ │ ├─ axios.js │ │ └─ employeeApi.js │ │ │ ├─ components │ │ └─ EmployeeForm.vue │ │ │ ├─ views │ │ ├─ EmployeeListView.vue │ │ ├─ EmployeeCreateView.vue │ │ ├─ EmployeeDetailView.vue │ │ └─ EmployeeEditView.vue │ │ │ ├─ router │ │ └─ index.js │ │ │ ├─ App.vue │ └─ main.js │ └─ package.json
15.6 기존 Repository 재사용
employee_repository.py
import sqlite3


DB_NAME = "employee.db"


def get_connection():

    conn = sqlite3.connect(
        DB_NAME
    )

    conn.row_factory = (
        sqlite3.Row
    )

    return conn


def find_all():

    conn = get_connection()
    cursor = conn.cursor()

    cursor.execute(
        """
        SELECT
            employee_id,
            name,
            phone,
            department
        FROM employee
        ORDER BY employee_id
        """
    )

    rows = cursor.fetchall()

    conn.close()

    return [
        dict(row)
        for row in rows
    ]
왜 그대로 사용할 수 있을까?
이 코드는 Tkinter의 Entry, Treeview, Window와 관계가 없습니다. 따라서 웹 프로그램의 Repository에서도 재사용할 수 있습니다.
15.7 이름 검색 기능 추가
Repository 검색 함수
def search_by_name(
    keyword
):

    conn = get_connection()
    cursor = conn.cursor()

    cursor.execute(
        """
        SELECT
            employee_id,
            name,
            phone,
            department
        FROM employee
        WHERE name LIKE ?
        ORDER BY employee_id
        """,
        (
            "%" + keyword + "%",
        )
    )

    rows = cursor.fetchall()

    conn.close()

    return [
        dict(row)
        for row in rows
    ]
웹에서 사용할 API
GET /employees/search?keyword=홍
15.8 검색 API 만들기
Router
employee_router.py
@router.get(
    "/search"
)
def search_employees(
    keyword: str = ""
):

    return (
        employee_service
        .search_employees(
            keyword
        )
    )
Service
employee_service.py
def search_employees(
    keyword
):

    keyword = keyword.strip()

    if keyword == "":

        return (
            employee_repository
            .find_all()
        )

    return (
        employee_repository
        .search_by_name(
            keyword
        )
    )
15.9 검색 URL의 의미
URL
/employees/search?keyword=홍길동

여기서 keywordQuery Parameter입니다.

FastAPI
def search_employees(
    keyword: str = ""
):
    ...
Vue ↓ keyword = 홍길동 ↓ GET /employees/search?keyword=홍길동 ↓ FastAPI ↓ search_by_name() ↓ SQLite
15.10 Vue 검색 API 함수
employeeApi.js
export function searchEmployees(
  keyword
) {

  return api.get(
    '/employees/search',
    {
      params: {
        keyword: keyword
      }
    }
  )

}
Axios가 만드는 요청 주소
/employees/search?keyword=홍길동
15.11 목록 화면에 검색 기능 추가
EmployeeListView.vue
<script setup>

import {
  ref,
  onMounted
} from 'vue'

import {
  getEmployees,
  searchEmployees,
  deleteEmployee
} from '../api/employeeApi'


const employees = ref([])
const keyword = ref('')


async function loadEmployees() {

  const response =
    await getEmployees()

  employees.value =
    response.data

}


async function search() {

  const response =
    await searchEmployees(
      keyword.value
    )

  employees.value =
    response.data

}


async function resetSearch() {

  keyword.value = ''

  await loadEmployees()

}


async function removeEmployee(
  id
) {

  if (
    !confirm(
      '삭제하시겠습니까?'
    )
  ) {
    return
  }

  await deleteEmployee(id)

  await loadEmployees()

}


onMounted(
  loadEmployees
)

</script>


<template>

  <div>

    <h2>
      사원 목록
    </h2>

    <div>

      <input
        v-model="keyword"
        placeholder="이름 검색"
      >

      <button
        @click="search"
      >
        검색
      </button>

      <button
        @click="resetSearch"
      >
        전체
      </button>

    </div>

    <br>

    <router-link
      to="/employees/new"
    >
      사원 등록
    </router-link>

    <br><br>


    <table border="1">

      <thead>
        <tr>
          <th>번호</th>
          <th>이름</th>
          <th>전화번호</th>
          <th>부서</th>
          <th>관리</th>
        </tr>
      </thead>

      <tbody>

        <tr
          v-for="emp in employees"
          :key="emp.employee_id"
        >

          <td>
            {{ emp.employee_id }}
          </td>

          <td>

            <router-link
              :to="
                '/employees/'
                + emp.employee_id
              "
            >
              {{ emp.name }}
            </router-link>

          </td>

          <td>
            {{ emp.phone }}
          </td>

          <td>
            {{ emp.department }}
          </td>

          <td>

            <router-link
              :to="
                '/employees/'
                + emp.employee_id
                + '/edit'
              "
            >
              수정
            </router-link>

            &nbsp;

            <button
              @click="
                removeEmployee(
                  emp.employee_id
                )
              "
            >
              삭제
            </button>

          </td>

        </tr>

      </tbody>

    </table>

  </div>

</template>
15.12 Tkinter 검색과 Vue 검색 비교
Tkinter
Python
def search():

    keyword = (
        entry_search.get()
    )

    employees = (
        repository
        .search_by_name(
            keyword
        )
    )

    clear_tree()

    for emp in employees:

        tree.insert(
            "",
            "end",
            values=emp
        )
Vue
Vue
async function search() {

  const response =
    await searchEmployees(
      keyword.value
    )

  employees.value =
    response.data

}
Tkinter Vue
Repository 직접 호출 API 호출
Treeview 직접 변경 employees 데이터 변경
화면을 코드로 수정 데이터 변경 후 화면 자동 갱신
15.13 등록 화면을 컴포넌트로 만들기

등록과 수정 화면에는 이름, 전화번호, 부서라는 동일한 입력 항목이 있습니다. 따라서 공통 컴포넌트로 분리할 수 있습니다.

components/EmployeeForm.vue
<script setup>

const props = defineProps({
  employee: Object,

  buttonText: {
    type: String,
    default: '저장'
  }
})


const emit = defineEmits([
  'save'
])


function save() {
  emit('save')
}

</script>


<template>

  <div>

    <p>
      이름

      <input
        v-model="
          props.employee.name
        "
      >
    </p>

    <p>
      전화번호

      <input
        v-model="
          props.employee.phone
        "
      >
    </p>

    <p>
      부서

      <select
        v-model="
          props.employee.department
        "
      >

        <option value="개발팀">
          개발팀
        </option>

        <option value="영업팀">
          영업팀
        </option>

        <option value="관리팀">
          관리팀
        </option>

      </select>

    </p>

    <button @click="save">
      {{ buttonText }}
    </button>

  </div>

</template>
15.14 등록 화면에서 컴포넌트 사용
EmployeeCreateView.vue
<script setup>

import {
  reactive
} from 'vue'

import {
  useRouter
} from 'vue-router'

import EmployeeForm
  from '../components/EmployeeForm.vue'

import {
  createEmployee
} from '../api/employeeApi'


const router = useRouter()


const employee = reactive({
  name: '',
  phone: '',
  department:
    '개발팀'
})


async function save() {

  try {

    await createEmployee(
      employee
    )

    alert(
      '등록되었습니다.'
    )

    router.push(
      '/employees'
    )

  } catch (error) {

    alert(
      error.response?.data?.detail
      ||
      '등록 중 오류가 발생했습니다.'
    )

  }

}

</script>


<template>

  <h2>
    사원 등록
  </h2>

  <EmployeeForm
    :employee="employee"
    button-text="등록"
    @save="save"
  />

</template>
15.15 수정 화면에서도 재사용하기
EmployeeEditView.vue
<script setup>

import {
  reactive,
  onMounted
} from 'vue'

import {
  useRoute,
  useRouter
} from 'vue-router'

import EmployeeForm
  from '../components/EmployeeForm.vue'

import {
  getEmployee,
  updateEmployee
} from '../api/employeeApi'


const route = useRoute()
const router = useRouter()


const employee = reactive({
  name: '',
  phone: '',
  department: ''
})


async function loadEmployee() {

  const response =
    await getEmployee(
      route.params.id
    )

  Object.assign(
    employee,
    response.data
  )

}


async function save() {

  try {

    await updateEmployee(
      route.params.id,
      employee
    )

    alert(
      '수정되었습니다.'
    )

    router.push(
      '/employees'
    )

  } catch (error) {

    alert(
      error.response?.data?.detail
      ||
      '수정 중 오류가 발생했습니다.'
    )

  }

}


onMounted(
  loadEmployee
)

</script>


<template>

  <h2>
    사원 수정
  </h2>

  <EmployeeForm
    :employee="employee"
    button-text="수정"
    @save="save"
  />

</template>
컴포넌트 재사용
등록 화면과 수정 화면이 같은 EmployeeForm 컴포넌트를 사용하게 됩니다.
Vue 컴포넌트와 웹 애플리케이션 개발
웹 전환에서는 화면을 페이지와 컴포넌트 단위로 나누어 재사용하는 구조가 중요합니다.
15.16 Python 프로그램의 메뉴 변환
Tkinter 메뉴
Python
menu = tk.Menu(
    window
)

employee_menu = tk.Menu(
    menu,
    tearoff=0
)

employee_menu.add_command(
    label="사원목록"
)

employee_menu.add_command(
    label="사원등록"
)

menu.add_cascade(
    label="사원관리",
    menu=employee_menu
)

window.config(
    menu=menu
)
Vue Router 메뉴
Vue
<nav>

  <router-link
    to="/employees"
  >
    사원목록
  </router-link>

  |

  <router-link
    to="/employees/new"
  >
    사원등록
  </router-link>

</nav>
15.17 Frame 화면 전환 제거
Tkinter
MainWindow │ ├─ EmployeeListFrame ├─ EmployeeCreateFrame └─ EmployeeEditFrame
Vue
Tkinter Vue
EmployeeListFrameEmployeeListView.vue
EmployeeCreateFrameEmployeeCreateView.vue
EmployeeEditFrameEmployeeEditView.vue

웹에서는 Frame을 앞으로 가져오는 대신 Vue Router가 화면 이동을 담당합니다.

15.18 messagebox 변경
안내 메시지
Tkinter
messagebox.showinfo(
    "확인",
    "등록되었습니다."
)
Web
alert(
  '등록되었습니다.'
)
삭제 확인
Tkinter
result = (
    messagebox.askyesno(
        "확인",
        "삭제하시겠습니까?"
    )
)
Web
const result = confirm(
  '삭제하시겠습니까?'
)
실제 서비스에서는
alert()confirm() 대신 별도의 Vue 모달 컴포넌트를 사용할 수도 있습니다.
15.19 기존 main.py 변경
Tkinter 실행 코드
기존 main.py
from ui import (
    EmployeeWindow
)


if __name__ == "__main__":

    app = EmployeeWindow()

    app.run()
FastAPI main.py
backend/main.py
from fastapi import FastAPI

from fastapi.middleware.cors import (
    CORSMiddleware
)

from routers.employee_router import (
    router as employee_router
)


app = FastAPI()


app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173"
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)


app.include_router(
    employee_router
)
Terminal
uvicorn main:app --reload
15.20 Vue의 시작점

기존 Python에서는 if __name__ == "__main__": 부분이 프로그램 실행의 시작점 역할을 했습니다.

Vue에서는 main.js가 시작점입니다.

frontend/src/main.js
import {
  createApp
} from 'vue'

import App
  from './App.vue'

import router
  from './router'


createApp(App)
  .use(router)
  .mount('#app')
15.21 기존 SQLite 데이터 그대로 사용하기
기존 employee 테이블
CREATE TABLE employee (
    employee_id INTEGER
        PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    phone TEXT,
    department TEXT
);

기존 DB 테이블 구조가 웹 프로그램에서 필요한 구조와 같다면 데이터베이스를 새로 만들 필요가 없습니다.

기존
Tkinter → employee.db
변경
Vue → FastAPI → employee.db
15.22 기존 데이터를 유지하는 장점
employee.db 사원 1,000명

웹 프로그램으로 변경한다고 해서 기존 데이터를 다시 입력할 필요는 없습니다. FastAPI Repository가 기존 DB 파일에 접근하도록 만들면 됩니다.

기존 SQLite → FastAPI → Vue
💡 데이터 이전이 필요하지 않을 수도 있음
기존 DB 구조가 그대로 사용 가능하다면 UI와 접근 구조만 변경하면서 기존 데이터를 유지할 수 있습니다.
15.23 DB 경로 관리하기
database.py
from pathlib import Path
import sqlite3


BASE_DIR = (
    Path(__file__)
    .resolve()
    .parent
)


DB_PATH = (
    BASE_DIR
    / "employee.db"
)


def get_connection():

    conn = sqlite3.connect(
        DB_PATH
    )

    conn.row_factory = (
        sqlite3.Row
    )

    return conn
경로를 이렇게 만드는 이유
프로그램을 어떤 위치에서 실행하더라도 database.py가 있는 폴더를 기준으로 DB 경로를 찾기 쉬워집니다.
15.24 환경설정 분리하기
frontend/.env
VITE_API_URL=http://localhost:8000
axios.js
import axios from 'axios'


const api = axios.create({
  baseURL:
    import.meta.env
      .VITE_API_URL
})


export default api
운영 환경 예
.env
VITE_API_URL=https://api.example.com

환경에 따라 API 주소만 변경할 수 있습니다.

15.25 로딩 상태 추가하기
Vue
const loading = ref(false)


async function loadEmployees() {

  loading.value = true

  try {

    const response =
      await getEmployees()

    employees.value =
      response.data

  } finally {

    loading.value = false

  }

}
Template
<p v-if="loading">
  데이터를 불러오는 중입니다.
</p>
15.26 데이터가 없을 때 처리하기
Vue Template
<tr
  v-if="
    employees.length === 0
  "
>

  <td colspan="5">
    등록된 사원이 없습니다.
  </td>

</tr>

목록이 있으면 기존처럼 v-for를 사용합니다.

Vue Template
<tr
  v-for="emp in employees"
  :key="emp.employee_id"
>
  ...
</tr>
15.27 입력값 1차 검증
Vue
async function save() {

  if (
    employee.name
      .trim() === ''
  ) {

    alert(
      '이름을 입력하세요.'
    )

    return
  }

  await createEmployee(
    employee
  )

}

사용자가 잘못 입력한 경우 서버 요청을 보내기 전에 바로 알려줄 수 있습니다.

15.28 서버의 2차 검증
Service
def validate_name(name):

    return (
        name.strip() != ""
    )
Vue 검증 → 사용자 편의 FastAPI 검증 → 실제 데이터 보호
⚠ Vue에서 검증해도 서버 검증은 유지
클라이언트 검증만 믿고 FastAPI 검증을 제거하면 안 됩니다.
15.29 상세 화면에 목록 버튼 추가
목록 링크
<router-link
  to="/employees"
>
  목록
</router-link>
수정 링크
<router-link
  :to="
    '/employees/'
    + employee.employee_id
    + '/edit'
  "
>
  수정
</router-link>

웹에서는 화면 이동을 URL 중심으로 구성할 수 있습니다.

15.30 삭제 후 화면 처리
상세 화면 삭제
async function remove() {

  if (
    !confirm(
      '삭제하시겠습니까?'
    )
  ) {
    return
  }

  await deleteEmployee(
    route.params.id
  )

  alert(
    '삭제되었습니다.'
  )

  router.push(
    '/employees'
  )

}
삭제 확인 → DELETE API → 삭제 완료 → 목록 페이지 이동
Python 데스크톱 프로그램 웹 전환 프로젝트
큰 프로그램도 기능을 하나씩 전환하면 복잡도를 낮추면서 안정적으로 웹으로 옮길 수 있습니다.
15.31 실제 웹 전환 작업 순서
1
기존 프로그램 실행
2
화면과 기능 확인
3
Tkinter 코드 구분
4
업무 로직 구분
5
DB 코드 구분
6
Repository 분리
7
Service 분리
8
FastAPI Router 작성
9
API 테스트
10
Vue 화면 작성
11
Axios 연결
12
Vue Router 연결
13
CRUD 테스트
14
기존 데이터 확인
15.32 한 번에 전체를 바꾸지 않기
1단계 사원 조회 ↓ 2단계 사원 등록 ↓ 3단계 사원 수정 ↓ 4단계 사원 삭제 ↓ 5단계 검색 ↓ 6단계 추가 기능
추천 방식
기존 프로그램이 클수록 한 번에 모든 기능을 바꾸기보다 하나의 기능을 완성하고 테스트한 뒤 다음 기능으로 넘어가는 것이 좋습니다.
15.33 조회 기능부터 변환하기
Vue ↓ GET /employees ↓ FastAPI ↓ find_all() ↓ SQLite

목록 화면이 정상적으로 출력되는 것을 확인한 뒤 다음 기능으로 넘어갑니다.

15.34 등록 기능 추가
Vue 입력 ↓ POST /employees ↓ FastAPI ↓ Service ↓ insert() ↓ SQLite

등록이 완료되면 목록 화면으로 이동합니다.

15.35 수정 기능 추가
GET /employees/1 ↓ 기존 데이터 표시 ↓ 사용자가 수정 ↓ PUT /employees/1 ↓ UPDATE
15.36 삭제 기능 추가
삭제 버튼 ↓ confirm() ↓ DELETE /employees/1 ↓ DELETE SQL ↓ 목록 다시 조회
15.37 검색 기능 추가
검색어 입력 ↓ GET /employees/search ↓ LIKE 검색 ↓ 검색 결과 반환 ↓ employees.value 변경
CRUD + 검색
검색 기능까지 구현하면 일반적인 업무용 CRUD 프로그램의 기본 형태가 완성됩니다.
15.38 다른 Python 프로그램에도 적용하기
회원관리
Tkinter 회원관리 ↓ Vue 회원관리 ↓ FastAPI ↓ Member Service ↓ Member Repository ↓ SQLite
재고관리
Vue → FastAPI → Inventory Service → Inventory Repository → Database
게시판
Vue → FastAPI → Board Service → Board Repository → Database
15.39 파일 관리 프로그램 전환
기존 Python
import os


files = os.listdir(
    "data"
)

Vue에서 서버 파일을 직접 읽지 않고 FastAPI API를 만듭니다.

FastAPI
@app.get("/files")
def get_files():

    return os.listdir(
        "data"
    )
Vue
const response =
  await api.get(
    '/files'
  )
Vue → FastAPI → Python 파일 처리
15.40 외부 API 프로그램 전환
기존 Python 함수 개념
def search_data():

    # 외부 API 호출

    return result
Vue → FastAPI → 기존 Python API 함수 → 외부 서비스
적용 예
유튜브 검색 · 주식 정보 · 날씨 정보 · AI API · 지도 API
15.41 Python 라이브러리도 계속 사용할 수 있다
기존 Python 라이브러리
pandas · openpyxl · requests · BeautifulSoup · OpenCV · Pillow · sqlite3 · psycopg2 · PyMySQL
Vue → FastAPI → pandas → Excel 처리
Vue → FastAPI → OpenCV → 이미지 처리
웹으로 바꿔도 Python의 장점은 유지
화면만 Vue로 바뀌고 서버는 Python을 사용하므로 기존 Python 라이브러리와 처리 로직을 계속 활용할 수 있습니다.
15.42 Tkinter와 Vue의 역할 차이
기존 Tkinter 프로그램 웹 프로그램
Python이 화면까지 담당 Vue가 화면 담당
Python 함수 직접 호출 FastAPI를 HTTP로 호출
Python이 업무 로직 처리 FastAPI / Python이 업무 로직 처리
Python이 DB 접근 FastAPI 서버의 Repository가 DB 접근
화면과 서버가 한 프로그램 프론트엔드와 백엔드 역할 분리
Vue 화면 · 입력 · 이벤트 · 페이지 이동 FastAPI / Python 업무 로직 · DB · 파일 · 계산 · 외부 API
15.43 웹 전환 시 삭제할 코드
Tkinter 코드 웹에서 대체
import tkinterVue / HTML / CSS
Tk()Vue 애플리케이션
Labellabel, p, span 등
Entry<input>
Button<button>
Frame<div>, Vue 컴포넌트
Treeview<table>, v-for
pack()CSS 레이아웃
grid()CSS Grid
place()CSS 위치 지정
mainloop()브라우저와 Vue
messageboxalert(), confirm() 또는 웹 UI
15.44 웹 전환 시 유지할 코드
기존 Python 코드 웹 전환 후
SQL대부분 유지
sqlite3 DB 처리서버에서 유지
계산 함수유지
데이터 검증 함수유지
파일 처리유지
Excel 처리유지
데이터 변환유지
외부 API 호출유지
Python 라이브러리대부분 유지
업무 규칙유지
판단 기준
화면에 의존하는 코드는 Vue로 바꾸고, 화면과 독립된 Python 코드는 가능한 한 재사용합니다.
15.45 가장 이상적인 기존 구조
전환 전
employee_manager │ ├─ ui │ └─ employee_window.py │ ├─ services │ └─ employee_service.py │ ├─ repositories │ └─ employee_repository.py │ ├─ models │ └─ employee.py │ └─ main.py
웹 전환 후
employee_web │ ├─ frontend │ └─ Vue │ └─ backend │ ├─ routers ├─ services ├─ repositories └─ models
좋은 구조의 장점
기존의 services, repositories, models가 이미 화면에서 분리되어 있다면 웹 전환 시 상당 부분 그대로 활용할 수 있습니다.
15.46 실습 프로젝트
🎯 실습 목표
기존 Tkinter 회원관리 프로그램을 Vue + FastAPI 웹 프로그램으로 변경합니다.
회원 테이블
CREATE TABLE member (
    member_id INTEGER
        PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    phone TEXT,
    address TEXT
);
구현할 기능
회원 목록 회원 등록 회원 상세 회원 수정 회원 삭제 회원 이름 검색
Backend
member_router.py member_service.py member_repository.py
Frontend
MemberListView.vue MemberCreateView.vue MemberDetailView.vue MemberEditView.vue MemberForm.vue
API
Method URL 기능
GET /members 회원 목록
GET /members/{id} 회원 상세
GET /members/search?keyword=홍 회원 검색
POST /members 회원 등록
PUT /members/{id} 회원 수정
DELETE /members/{id} 회원 삭제

화면 주소는 /member 또는 /members 형태로 구성할 수 있습니다.

15.48 최종 프로그램 구조
Browser │ ▼ Vue (ListView / CreateView / DetailView / EditView) (Component / Router) │ ▼ Axios / JSON │ ▼ FastAPI Router │ ▼ Service (검증 / 계산 / 업무 처리) │ ▼ Repository (SELECT / INSERT / UPDATE / DELETE) │ ▼ SQLite
기존 코드 판단 웹 전환 방법
화면 코드인가? Vue로 변경
Python 업무 코드인가? Service에서 재사용
DB 코드인가? Repository에서 재사용
외부에서 호출해야 하는가? FastAPI Router 추가
⭐ Chapter 15의 핵심 판단법
기존 Python 프로그램을 웹으로 전환할 때는 코드를 무조건 다시 작성하는 것이 아니라 어떤 역할의 코드인지 먼저 판단한 뒤 알맞은 위치로 이동시키는 것이 중요합니다.
📌 Chapter 15 핵심 정리
  • Python/Tkinter 프로그램을 웹으로 전환할 때 전체 프로그램을 처음부터 다시 만들 필요는 없습니다.
  • 가장 먼저 기존 코드를 UI · 업무 로직 · DB 코드로 구분합니다.
  • Tkinter의 Entry, Button, Treeview, Frame, messagebox 같은 화면 종속 코드는 Vue로 변경합니다.
  • 검증, 계산, 데이터 가공 같은 기존 Python 업무 로직은 Service에서 재사용할 수 있습니다.
  • SQL과 sqlite3 기반 데이터 접근 코드는 Repository에서 재사용할 수 있습니다.
  • Vue는 Repository를 직접 호출하지 않고 Axios → FastAPI Router를 통해 서버에 요청합니다.
  • 이름 검색은 Query Parameter를 이용해 /employees/search?keyword=홍길동 형태로 구현할 수 있습니다.
  • 등록과 수정 화면의 입력 항목이 같다면 EmployeeForm.vue와 같은 공통 컴포넌트로 분리할 수 있습니다.
  • Tkinter 메뉴와 Frame 화면 전환은 Vue Router와 router-link로 변경합니다.
  • 기존 SQLite 테이블 구조가 그대로 사용 가능하면 DB와 기존 데이터를 새로 만들지 않고 유지할 수 있습니다.
  • DB 경로는 Path(__file__).resolve().parent와 같이 파일 기준으로 관리하면 실행 위치에 따른 문제를 줄일 수 있습니다.
  • API 주소는 .envVITE_API_URL로 분리할 수 있습니다.
  • Vue의 검증은 사용자 편의를 위한 1차 검증, FastAPI의 검증은 데이터 보호를 위한 2차 검증으로 사용할 수 있습니다.
  • 큰 프로그램은 조회 → 등록 → 수정 → 삭제 → 검색 순서처럼 기능별로 하나씩 전환하는 것이 좋습니다.
  • 파일 처리, Excel 처리, 이미지 처리, 외부 API 호출 등 기존 Python 기능도 FastAPI 서버 뒤에서 계속 사용할 수 있습니다.
  • 최종 구조는 Browser → Vue → Axios → FastAPI Router → Service → Repository → SQLite 로 이해할 수 있습니다.
  • 가장 중요한 기준은 화면은 Vue, 업무 로직은 Service, DB는 Repository, 외부 호출 지점은 FastAPI Router 로 역할을 나누는 것입니다.

프로그램·홈페이지·강의가 필요하신가요?

프로그램 판매, 무료 다운로드, 홈페이지 제작, IT 강의 상담을 도와드립니다.

상담 신청하기