Svelte : 심플한 "To-Do 리스트" Svelte + PHP + MySQL

2025. 5. 15. 12:46Svelte

목표

  • Svelte로 할 일 목록 추가/삭제/완료 체크
  • PHP API로 MySQL CRUD 연결
  • 초간단 풀스택 SPA 만들기

 

심플한 "To-Do 리스트" with Svelte + PHP + MySQL

  • 기능: 할 일을 추가/삭제/완료 체크 (SPA)
  • 구성:
    • Svelte로 프론트엔드 구현
    • PHP로 CRUD API
    • MySQL에 할 일 저장
  • 목표: Svelte의 상태관리 & PHP API + DB 연동 연습

 

To-Do 리스트
Svelte + PHP CRUD

 

🛠️ 개발 순서 백엔드 -> 프론트엔드

 

백엔드 만들기 시작

1️⃣ MySQL 테이블 만들기 (attendance.sql)

CREATE DATABASE todo_app;

USE todo_app;

CREATE TABLE todos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    task TEXT NOT NULL,
    completed BOOLEAN DEFAULT FALSE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

 

 

2️⃣ PHP 서버 API 만들기

config.php (DB연결)

<?php
$host = "localhost";
$user = "root";
$pass = "1234"; // 비밀번호 수정
$db = "todo_app";

$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}
?>

 

 

 

getTodos.php

<?php
include("../db/config.php");

$result = $conn->query("SELECT * FROM todos ORDER BY created_at DESC");

$todos = [];
while($row = $result->fetch_assoc()) {
    $todos[] = $row;
}

echo json_encode($todos);
?>

 

addTodo.php

<?php
include("../db/config.php");

$task = $_POST['task'] ?? '';

if ($task) {
    $stmt = $conn->prepare("INSERT INTO todos (task) VALUES (?)");
    $stmt->bind_param("s", $task);
    $stmt->execute();
    echo json_encode(["status" => "success"]);
} else {
    echo json_encode(["status" => "no task"]);
}
?>

 

deleteTodo.php

<?php
include("../db/config.php");

$id = $_POST['id'] ?? '';

if ($id) {
    $stmt = $conn->prepare("DELETE FROM todos WHERE id = ?");
    $stmt->bind_param("i", $id);
    $stmt->execute();
    echo json_encode(["status" => "success"]);
} else {
    echo json_encode(["status" => "no id"]);
}
?>

 

 

toggleTodo.php

<?php
include("../db/config.php");

$id = $_POST['id'] ?? '';

if ($id) {
    $stmt = $conn->prepare("UPDATE todos SET completed = NOT completed WHERE id = ?");
    $stmt->bind_param("i", $id);
    $stmt->execute();
    echo json_encode(["status" => "success"]);
} else {
    echo json_encode(["status" => "no id"]);
}
?>

 

 

3️⃣ Svelte 프론트엔드 만들기

기본 프로젝트 생성

npx degit sveltejs/template frontend
cd frontend
npm install
npm run dev

 

 

Todo.svelte 예시 (코드블럭 선택에서 svelte가 없어서 javascript 선택함)

<script>
    import { onMount } from 'svelte';
    let todos = [];
    let newTask = '';

    async function loadTodos() {
        const res = await fetch('http://your-server-ip/backend/api/getTodos.php');
        todos = await res.json();
    }

    async function addTodo() {
        await fetch('http://your-server-ip/backend/api/addTodo.php', {
            method: 'POST',
            body: new URLSearchParams({ task: newTask })
        });
        newTask = '';
        loadTodos();
    }

    async function deleteTodo(id) {
        await fetch('http://your-server-ip/backend/api/deleteTodo.php', {
            method: 'POST',
            body: new URLSearchParams({ id })
        });
        loadTodos();
    }

    async function toggleTodo(id) {
        await fetch('http://your-server-ip/backend/api/toggleTodo.php', {
            method: 'POST',
            body: new URLSearchParams({ id })
        });
        loadTodos();
    }

    onMount(loadTodos);
</script>

<h1>📝 To-Do List</h1>
<input bind:value={newTask} placeholder="할 일 입력" />
<button on:click={addTodo}>추가</button>

<ul>
    {#each todos as todo}
        <li>
            <input type="checkbox" checked={todo.completed == 1} on:change={() => toggleTodo(todo.id)} />
            {todo.task}
            <button on:click={() => deleteTodo(todo.id)}>삭제</button>
        </li>
    {/each}
</ul>

 

 

✅ 결과물 정리

구성 요소완료 목표

🖥️ Svelte To-Do SPA 상태관리, 이벤트 처리
🌐 PHP API CRUD 기능 구현
🗄️ MySQL todos 테이블 구축
🛠️ Svelte ↔ PHP API 연결 fetch 통한 동기화

 

 

✅ 끝나면 이런 느낌

  • Svelte로 CRUD SPA 직접 만들어보기
  • PHP API & DB 연동 경험
  • 실제 서비스 흐름 체험 (SPA → API → DB)

 

👍 구현 과정 스크린샷

1️⃣ 준비물 설치

✅ MySQL + PHP 환경 준비

  • 로컬에서 XAMPP 설치 (MySQL + Apache 통합 환경)
  • MySQL 접속 (phpMyAdmin 또는 DBeaver)
  • 여기도 접속되면 DB까지 준비 완료
  • 실행되면 http://localhost:5173 에서 확인 가능

다운받은 XAMPP Control Panel 실행 후 Apache, MySQL Start 누르기

 

✅ XAMPP가 뭐하는 놈이냐?

👉 "내 컴퓨터를 웹서버처럼 만들어주는 패키지"

🔽 구체적으로 말하면:

구성 요소역할쉽게 말하면
Apache 웹 서버 내 PC를 작은 인터넷 사이트처럼 동작시킴
MySQL 데이터베이스 데이터 저장소 (회원정보, 게시판 글 등)
PHP 서버 스크립트 언어 DB와 연결해서 웹페이지를 동적으로 만듦
phpMyAdmin DB 관리 툴 MySQL을 웹 브라우저에서 쉽게 조작
 

🟡 XAMPP는 위 4개를 "한 번에 설치하고 바로 쓸 수 있게" 묶은 패키지입니다.

  • 원래는 다 따로 설치하고 설정해야 하는데 귀찮죠?
  • XAMPP는 그냥 1번 설치로 내 PC를 서버처럼 만들어줌.

✅ 정리: 내 로컬에서 이런 걸 하고 싶을 때 씁니다

  • PHP + MySQL 웹 개발 테스트 (ex: To-Do 리스트 만들기)
  • 서버 없이도 "http://localhost"로 내 웹사이트 돌려보기
  • 실제 서버 배포 전에 로컬에서 연습/테스트

✅ 외우기 쉽게

"XAMPP = 내 컴퓨터를 미니 웹서버로 만들어주는 간편 설치세트"

 

 

  • 👇아래 : 설치 시 경고 메세지 (정확하게는 설치하려고 하는 데 에러 메세지 뜸)

C:\Program Files에서 설치하면 문제 생길 수 있으니 C드라이브 바로 밑으로 옮겨라 라는 뜻

🟡 경고 메시지 해석

"UAC(사용자 계정 컨트롤) 때문에 XAMPP가 제대로 동작하지 않을 수 있다. 특히 C:\Program Files 폴더에 설치하면 권한 문제로 쓰기 제한이 걸릴 수 있으니 다른 경로에 설치해라. 아니면 UAC를 꺼라."


✅ 해결 방법 (추천)

  1. C:\xampp 또는 D:\xampp 처럼
    Program Files 폴더가 아닌 곳에 설치하면 문제 없음.
    • 경로 예: C:\xampp ✅
    • 경로 예: C:\Program Files\xampp ❌ (하지 말라는 것)
  2. UAC(사용자 계정 컨트롤)는 굳이 건드리지 않아도 됩니다.
    • 그냥 경로만 조심하면 됩니다.
  3. 설치 컴포넌트 선택
    • 그냥 기본 설정 그대로 두고 "Next"
      • Apache (웹서버) ✅
      • MySQL ✅
      • PHP ✅
      • phpMyAdmin ✅
      • 기타는 굳이 건드리지 않아도 OK

    4. 설치 진행 → Finish
    • "Do you want to start the Control Panel now?" 체크된 상태로 Finish 클릭

5.  XAMPP Control Panel 실행

  • Apache → Start ✅
  • MySQL → Start ✅
    • 두 개가 초록불로 바뀌면 성공!

6. 정상 작동 확인

PHP phpMyAdmin 확인 :  http://localhost/phpmyadmin

 

PHP SQL 작성 후

 

 

☑️ PHP API 만들기

✅ 폴더 구조 (XAMPP htdocs 기준)

C:\xampp\htdocs\todo-backend\
├── db\config.php
├── api\getTodos.php
├── api\addTodo.php
├── api\deleteTodo.php
└── api\toggleTodo.php

(코드블럭에 arduino 없어서 javascript 선택함)

 

☑️ Postman 사용 (정석 방법)

Postman은 API 테스트하는 무료 프로그램입니다.

  1. Postman 설치 (https://www.postman.com/downloads/)
  2. 새 요청 만들기
  3. Method: POST
  4. URL: http://localhost/todo-backend/api/addTodo.php
  5. Body → form-data 선택
    Key: task, Value: 공부하기
  6. Send 버튼
  7. 결과가나오면 성공
  8. json
    복사편집
    {"status":"success"}

✅ 간단 정리

방법쉬움정확성
주소창 GET 테스트 👍 매우 쉬움 테스트용 임시
Postman 테스트 실무처럼 정확 추천

POST MAN 사용모습 - API를 쉽게 보내고 받는

 

나는 메모장에 하나하나씩 붙여넣고 확장자를 PHP 파일로 변환해서 만듦

[메모장에 만든 예시 하나만] toggleTodo.php 만들기 (완료 여부 토글 API)

➡️ 위치: C:\xampp\htdocs\todo-backend\api\toggleTodo.php

➡️ 파일 내용:

<?php
include("../db/config.php");

$id = $_POST['id'] ?? '';

if ($id) {
    $stmt = $conn->prepare("UPDATE todos SET completed = NOT completed WHERE id = ?");
    $stmt->bind_param("i", $id);
    $stmt->execute();
    echo json_encode(["status" => "success"]);
} else {
    echo json_encode(["status" => "no id"]);
}
?>

 

🔹 설명

  • 할 일 ID를 받아서, completed 필드를 0 → 1 또는 1 → 0 으로 반전시킴 (체크 토글)
  • 성공 시 {"status":"success"} 반환

✅ 테스트 방법 (Postman)

  1. 새 요청 → Method: POST
  2. URL: http://localhost/todo-backend/api/toggleTodo.php
  3. Body → form-data →
    Key: id, Value: (토글할 할 일 id 값, ex: 1)
  4. Send 버튼 클릭
  5. 결과 → {"status":"success"} 나오면 성공

→ 다시 getTodos.php로 조회하면 completed 값이 0 → 1 로 바뀐 걸 확인할 수 있습니다.

 

>> 이런 식으로 따라했음!

 

 ☑️ Svelte 프론트엔드 만들기

App.svelte에서 작성하고 실행하라고 함

👉 원래는 +page.svelte에서 작성하고 page를 import해서 가져다가 보여주는 방법을 쓰려고 했으나, GPT-4o가 App에서 작성해서 띄우라고 하길래 그렇게 계획을 변경했다.

코드 - 나는 VsCode 사용함

  • API 4개 사용 - CRUD
<script>
    import { onMount } from 'svelte';
    let todos = [];
    let newTask = '';

    async function loadTodos() {
        const res = await fetch('http://localhost/todo-backend/api/getTodos.php');
        todos = await res.json();
    }

    async function addTodo() {
        if (!newTask) return;

        await fetch('http://localhost/todo-backend/api/addTodo.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({ task: newTask })
        });

        newTask = '';
        loadTodos();
    }

    async function deleteTodo(id) {
        await fetch('http://localhost/todo-backend/api/deleteTodo.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({ id })
        });

        loadTodos();
    }

    async function toggleTodo(id) {
        await fetch('http://localhost/todo-backend/api/toggleTodo.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({ id })
        });

        loadTodos();
    }

    onMount(loadTodos);
</script>

<main>
    <h1>📝 To-Do List</h1>

    <div class="input-area">
        <input bind:value={newTask} placeholder="할 일 입력" />
        <button on:click={addTodo}>추가</button>
    </div>

    <ul>
        {#each todos as todo}
            <li class:completed={todo.completed == 1}>
                <input type="checkbox" checked={todo.completed == 1} on:change={() => toggleTodo(todo.id)} />
                {todo.task}
                <button on:click={() => deleteTodo(todo.id)}>삭제</button>
            </li>
        {/each}
		 
    </ul>

	<h2>🔎 디버그용 개별 확인</h2>
    {#if todos[0]}
        <p>첫 번째 항목: {todos[0].task} (완료: {todos[0].completed == 1 ? 'O' : 'X'})</p>
    {/if}
    {#if todos[1]}
        <p>두 번째 항목: {todos[1].task} (완료: {todos[1].completed == 1 ? 'O' : 'X'})</p>
    {/if}
</main>

<style>
    main {
        text-align: center;
        padding: 2rem;
        max-width: 600px;
        margin: 0 auto;
    }

    h1 {
        color: #ff3e00;
        text-transform: uppercase;
        font-size: 3rem;
        margin-bottom: 2rem;
    }

    .input-area {
        display: flex;
        justify-content: center;
        gap: 10px;
        margin-bottom: 20px;
    }

    input[type="text"] {
        width: 60%;
        padding: 0.5rem;
        font-size: 1rem;
    }

    button {
        padding: 0.5rem 1rem;
        background: #ff3e00;
        color: white;
        border: none;
        cursor: pointer;
        border-radius: 5px;
    }

    ul {
        list-style: none;
        padding: 0;
    }

    li {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0.5rem;
        border-bottom: 1px solid #ddd;
    }

    li.completed {
        text-decoration: line-through;
        color: #999;
    }

    li button {
        background: #ccc;
        color: #333;
        border: none;
        padding: 0.3rem 0.7rem;
        border-radius: 3px;
        cursor: pointer;
    }
</style>

 

{#each todos as todo}
            <li class:completed={todo.completed == 1}>
                <input type="checkbox" checked={todo.completed == 1} on:change={() => toggleTodo(todo.id)} />
                {todo.task}
                <button on:click={() => deleteTodo(todo.id)}>삭제</button>
            </li>
        {/each}

 

{#each todos as todo} >> 이 부분이 for 문임

todo.completed 라고만 되어있어서 할일 생성하면 모두 다 완료 표시가 된 채로 생성됬음.

todo.completed 에  ==1 추가해서 수정 

웃긴 건, 기본 값이 1이 안한 거고 0이 한 걸로 되어있었음....

보통 0이 안한거고 1이 한건데... ㅋㅋㅋㅋㅋ

GPT-4o도 100% 믿으면 안된다는 것을 실감했다.

✅ 실행 결과

  • Svelte 화면에서 할 일 추가
  • 체크박스 클릭하면 완료 여부 변경
  • 삭제 버튼으로 할 일 삭제
  • 모두 MySQL 연동 (실시간 CRUD)

 

✅ 마무리 체크리스트

완료 여부항목
MySQL todos 테이블 생성
PHP API 4종 구현 (조회, 추가, 삭제, 토글)
Svelte SPA 기본 셋업
Svelte ↔ PHP API 연동
리스트 추가/삭제/완료 체크 UI 완성

 

귀여운 todo web app 을 만들어서 기뻤다 ㅋㅋ 아주 귀엽다 ㅋㅋ

완성된 웹앱 결과물

 

 

실행 결과 영상

 

💻소감😊

svelte 라는 언어를 사용해서 처음으로 만들어본 웹앱. 전부터 개인프로젝트 해보고 싶었는데 어제 처음 시작을 해봐서 기분이 좋다 ㅎㅎ 오늘은 날씨 앱을 만들어 볼 건데 출근하기 전까지 매일 하루에 하나씩 만드는 게 목표다. 담주 월요일이 첫 출근 ^^ 대표님은 신기술을 가장 먼저 사용해보고 싶어하시는 분이고 직원들도 그런 태도를 가졌으면 좋겠다고 하셨다. 나도 새로운 것을 아주 잘 받아들이는 사람이니까 성향은 비슷하다고 느껴졌다. 스벨트 사용해보니 리액트보다도 코드가 훨씬 간결하고 마치 Streamlit으로 웹앱을 만드는 느낌이 났다~! 물론 그 땐 리액트로 만들었지만 사용하는 언어가 달라진다는 게 아주 재밌는 것 같다. 여러 나라의 언어를 짧은 시간안에 사용하는 것은 어렵지만 로직안에서 문법이 조금씩 변하는 컴퓨터 언어는 오히려 쉽게 다가오는 것 같다. 내가 언어쪽으로는 받아들이는 게 빠른 사람이라서 그런지 컴퓨터 언어도 결국 언어라서 나랑 쉽게 친해지는 것 같다~ 즐겁게 배우자! 마음은 가볍게 가볍게~ 매일 조금씩 나아지고 발전하고 있다는 느낌을 가지고^^ 내가 지금은 실력이 부족해도 기회만 주어진다면 얼마든지 노력해서 발전하고 성장할 수 있다! 라는 자신감을 가진다면 무엇이든 해낼 수 있다는 긍정마인드를 가지자!  아자아자~~!