이제 이론은 충분히 다뤘으니,
“실제 하나의 기능을 처음부터 끝까지 만들어 보는 단계” 로 가보자.
이번 글에서는 순수 자바스크립트만 가지고:
- 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을 브라우저에서 열면:
- 초기 유저 목록 두 명이 보이고
- 이름/이메일 입력 후 “추가” → 목록 갱신
- 각 유저 옆 “삭제” 버튼으로 제거 가능
간단하지만 비동기 + 모듈 + 클래스 + 계층 구조를 모두 포함한 완전한 미니 프로젝트다.
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”가 된다.
'frontend > javascript' 카테고리의 다른 글
| 🟨 2-2. 실전 이벤트 패턴 — 모달, 드롭다운, 아코디언, 탭 메뉴 한 번에 완성하기 (0) | 2025.11.07 |
|---|---|
| 🟨 2-1. 자바스크립트 이벤트 시스템의 모든 것 (0) | 2025.11.06 |
| 🟨 1-15. 모듈화 + 객체지향 설계로 프로젝트 구조 설계하기 — 폴더 구조부터 설계 철학까지 (0) | 2025.11.06 |
| 🟨 1-14. 객체지향 설계 심화 — 캡슐화, 상속, 추상화, 다형성을 자바스크립트로 구현하기 (0) | 2025.11.06 |
| 🟨 1-13. ES6 클래스(Class)와 프로토타입(Prototype) — 객체지향 자바스크립트의 핵심 구조 (0) | 2025.11.06 |