frontend/javascript

🟨 1-16. ES6 모듈 + 비동기 + 클래스 기반으로 미니 프로젝트 만들기 (User CRUD 실습)

mirabo01 2025. 11. 6. 22:14

이제 이론은 충분히 다뤘으니,
“실제 하나의 기능을 처음부터 끝까지 만들어 보는 단계” 로 가보자.

이번 글에서는 순수 자바스크립트만 가지고:

  • ES6 모듈 (import / export)
  • 클래스(class)
  • 비동기 처리 (async / await)
  • 간단한 설계 구조 (Model / Service / UI)

를 모두 엮어서 User 관리 미니 프로젝트 (CRUD) 를 직접 구현해본다.

“여기서 한 번 제대로 만들어보면, 이후 React·Vue로 가도 훨씬 이해가 빨라진다.”


1. 이번에 만들 기능 정리

간단한 “유저 관리” 기능을 만든다.

  • 유저 목록 조회 (Read)
  • 유저 추가 (Create)
  • 유저 삭제 (Delete)
  • 나중에 수정(Update)까지 확장 가능하도록 구조 설계

실제 서버는 없으니, 가짜 API(mock API) 를 만들어
fetch + setTimeout 으로 비동기 느낌을 그대로 살려볼 거다.


2. 폴더 구조 설계

먼저 파일 구조부터 잡자.

project/
 ┣ index.html
 ┗ src/
    ┣ api/
    │  ┗ UserApi.js
    ┣ models/
    │  ┗ User.js
    ┣ services/
    │  ┗ UserService.js
    ┣ ui/
    │  ┗ UserUI.js
    ┗ main.js
  • api/UserApi.js : 서버 대신 역할을 하는 가짜 API 모듈
  • models/User.js : User 데이터를 정의하는 모델 클래스
  • services/UserService.js : 비즈니스 로직 (CRUD)
  • ui/UserUI.js : DOM 조작, 화면 렌더링 담당
  • main.js : 초기화 및 전체 연결

구조는 일부러 “작은 MVC 느낌”으로 잡았다.
이 정도 구조면 나중에 프레임워크로 옮겨도 패턴이 자연스럽게 이어진다.


3. index.html — 가장 단순한 UI 뼈대

우선 HTML부터 만들자.

<!-- project/index.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <title>User 관리 미니 프로젝트</title>
</head>
<body>
  <h1>User 관리</h1>

  <section>
    <h2>유저 추가</h2>
    <input id="nameInput" placeholder="이름" />
    <input id="emailInput" placeholder="이메일" />
    <button id="addBtn">추가</button>
  </section>

  <section>
    <h2>유저 목록</h2>
    <ul id="userList"></ul>
  </section>

  <script type="module" src="./src/main.js"></script>
</body>
</html>

포인트:

  • type="module" 로 ES6 모듈 사용
  • 이름/이메일 입력 후 “추가” 버튼
  • 유저 목록은 <ul id="userList"> 에 렌더링

4. User 모델 — 데이터 구조 정의

이제 User 를 클래스로 정의해보자.

// src/models/User.js
export default class User {
  #id;
  #name;
  #email;

  constructor(id, name, email) {
    this.#id = id;
    this.#name = name;
    this.#email = email;
  }

  get id() {
    return this.#id;
  }

  get name() {
    return this.#name;
  }

  get email() {
    return this.#email;
  }

  rename(newName) {
    if (!newName || newName.length < 2) {
      throw new Error("이름은 두 글자 이상이어야 합니다.");
    }
    this.#name = newName;
  }

  changeEmail(newEmail) {
    if (!newEmail.includes("@")) {
      throw new Error("유효한 이메일을 입력하세요.");
    }
    this.#email = newEmail;
  }
}

여기서 의도는:

  • id, name, email 을 캡슐화(private 필드)
  • 게터(getter)로만 외부에 노출
  • 이름/이메일 변경은 메서드를 통해 검증 후 변경

즉, 데이터를 “그냥 객체로 내던지는 게 아니라, 규칙을 가진 모델” 로 다룬다.


5. UserApi — 가짜 비동기 API 만들기

실제 서버 대신, 메모리에 데이터를 들고 있는 간단한 mock API를 만들자.
비동기 느낌을 살리기 위해 Promise + setTimeout을 사용한다.

// src/api/UserApi.js
let users = [
  { id: 1, name: "기범", email: "kibum@example.com" },
  { id: 2, name: "민수", email: "minsu@example.com" },
];

let nextId = 3;

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

export default class UserApi {
  static async getUsers() {
    await delay(300); // 0.3초 지연 (비동기 느낌)
    return [...users]; // 복사본 반환
  }

  static async addUser(name, email) {
    await delay(300);

    const newUser = { id: nextId++, name, email };
    users.push(newUser);
    return newUser;
  }

  static async deleteUser(id) {
    await delay(300);
    users = users.filter(user => user.id !== id);
    return true;
  }
}

여기서:

  • users 배열이 서버 DB 역할
  • delay() 로 네트워크 지연 흉내
  • getUsers, addUser, deleteUser 는 모두 async 메서드

실제 프로젝트에서는 여기에 fetch 를 써서 진짜 API를 호출하면 된다.


6. UserService — 비즈니스 로직 계층

이제 API를 직접 UI에서 쓰지 않고,
중간에 “서비스 계층”을 둔다.

이렇게 하면 나중에 API가 바뀌어도
UI는 건드리지 않고 서비스만 수정하면 된다.

// src/services/UserService.js
import UserApi from "../api/UserApi.js";
import User from "../models/User.js";

export default class UserService {
  async getAllUsers() {
    const data = await UserApi.getUsers();
    return data.map(user => new User(user.id, user.name, user.email));
  }

  async createUser(name, email) {
    if (!name || !email) {
      throw new Error("이름과 이메일을 모두 입력하세요.");
    }

    const newUser = await UserApi.addUser(name, email);
    return new User(newUser.id, newUser.name, newUser.email);
  }

  async removeUser(id) {
    await UserApi.deleteUser(id);
  }
}

설계 포인트:

  • API에서 받은 순수 객체를 항상 User 인스턴스로 변환
  • UI 단에서는 “User 인스턴스”만 다루면 됨
  • 검증(이름/이메일 유효성 검사)도 이 계층에서 처리

7. UserUI — DOM 조작과 이벤트 처리

이제 화면을 실제로 업데이트하는 UI 계층을 만들자.

// src/ui/UserUI.js
import UserService from "../services/UserService.js";

export default class UserUI {
  #service;
  #userListEl;
  #nameInputEl;
  #emailInputEl;
  #addBtnEl;

  constructor() {
    this.#service = new UserService();

    this.#userListEl = document.getElementById("userList");
    this.#nameInputEl = document.getElementById("nameInput");
    this.#emailInputEl = document.getElementById("emailInput");
    this.#addBtnEl = document.getElementById("addBtn");

    this.#bindEvents();
  }

  #bindEvents() {
    this.#addBtnEl.addEventListener("click", async () => {
      const name = this.#nameInputEl.value.trim();
      const email = this.#emailInputEl.value.trim();

      try {
        await this.#service.createUser(name, email);
        this.#nameInputEl.value = "";
        this.#emailInputEl.value = "";
        await this.renderList();
      } catch (e) {
        alert(e.message);
      }
    });
  }

  async renderList() {
    const users = await this.#service.getAllUsers();

    this.#userListEl.innerHTML = "";

    users.forEach(user => {
      const li = document.createElement("li");
      li.textContent = `${user.name} (${user.email})`;

      const deleteBtn = document.createElement("button");
      deleteBtn.textContent = "삭제";
      deleteBtn.style.marginLeft = "8px";

      deleteBtn.addEventListener("click", async () => {
        if (!confirm(`${user.name}을(를) 삭제할까요?`)) return;
        await this.#service.removeUser(user.id);
        await this.renderList();
      });

      li.appendChild(deleteBtn);
      this.#userListEl.appendChild(li);
    });
  }
}

여기서 구조를 잘 보면:

  • UI는 Service 에만 의존
  • Service는 API + Model 에 의존
  • UI는 DOM 요소만 알고, 데이터 구조(User 인스턴스)는 Service에서 받는다

이렇게 역할을 분리해두면,
시간이 지나도 “어디를 고쳐야 할지” 헷갈리지 않게 된다.


8. main.js — 앱 초기화

마지막으로 모든 걸 연결해주자.

// src/main.js
import UserUI from "./ui/UserUI.js";

document.addEventListener("DOMContentLoaded", async () => {
  const ui = new UserUI();
  await ui.renderList();
});
  • 문서가 로드되면 UserUI 인스턴스를 생성
  • 초기 유저 목록 렌더링

이제 index.html을 브라우저에서 열면:

  1. 초기 유저 목록 두 명이 보이고
  2. 이름/이메일 입력 후 “추가” → 목록 갱신
  3. 각 유저 옆 “삭제” 버튼으로 제거 가능

간단하지만 비동기 + 모듈 + 클래스 + 계층 구조를 모두 포함한 완전한 미니 프로젝트다.


9. 여기서 무엇을 배웠나 정리

지금까지 만든 구조를 한 번 요약해 보면:

  • Model (User)
    • 데이터 형태와 제약 조건(검증)을 갖는 “하나의 유저” 정의
  • API (UserApi)
    • 서버 역할을 하는 비동기 인터페이스 (실제 서버로 교체 가능)
  • Service (UserService)
    • 비즈니스 로직 담당, Model과 API를 연결하는 계층
  • UI (UserUI)
    • 화면 렌더링 + 이벤트 처리 담당, 데이터 구조에는 관여하지 않음
  • main.js
    • 앱 초기화

이 구조는 규모가 조금만 더 커져도 그대로 확장할 수 있다.

예를 들어:

  • 수정(Update) 기능 추가 → UserService 에 updateUser 하나 추가
  • 검색 기능 추가 → UserService에 searchUsers 추가 + UserUI 에 검색 UI 추가
  • 실제 서버 연동 → UserApi 의 구현만 fetch 기반으로 변경

UI / Service / Model / API 가 분리되어 있으니, 어느 한쪽만 수정해도 전체가 깨지지 않는다.


10. 마무리

지금 만든 건 아주 작은 예제지만,
실제로 프론트엔드 프로젝트의 기본 구조와 사고 방식이 그대로 담겨 있다.

  • “그냥 동작하는 코드”에서
  • “역할이 명확하고 유지보수 가능한 코드”로 넘어가는 첫 단계다.

다음 단계로는 이 구조에:

  • 로딩 상태(spinner) 처리
  • 에러 메시지 UI 표시
  • 폼 검증 고도화

같은 것들을 하나씩 붙여 나가면,
곧바로 “실전용 CRUD UI”가 된다.