우리가 만든 코드를(우리가 아니라 내가 혼자 만들긴 함ㅋ) 다음 순서로 살펴볼 것이다.
💡 index.html
프론트엔드의 기본 UI와 구조를 정의한다. 특별한 건 없다.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Simple To-Do App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>To-Do List</h1>
<form id="todoForm">
<input type="text" id="taskInput" placeholder="Enter a task" required />
<button type="submit">Add Task</button>
</form>
<ul id="todoList"></ul>
<script src="app.js"></script>
</body>
</html>
분해해서 보면,
첫 번째 줄은 HTML5 문서임을 선언한다.
이는 HTML의 가장 최신 버전이고 따로 버전 번호를 명시하지 않아도 된다. 꾸준히 업데이트되지만 버전 번호가 바뀌지도 않는다.
두 번째 줄은 영어로 작성됨을 말한다.
한국어로 바꾸려면 `ko`라고 입력하면 된다. `en`이라 쓰여있어도 한국어를 문서에 작성할 수 있다. `lang` 속성은 검색 엔진이나 스크린 리더 같은 도구들이 문서의 기본 언어를 알 수 있도록 알려주는 용도다. 웹 페이지가 여러 언어로 제공된다면 브라우저는 이걸 참고해서 사용자에게 적절한 언어 설정을 자동으로 적용할 수 있다.
<head> 태그는 문서의 메타 정보와 설정을, <body>는 실제 화면에 보이는 콘텐츠를 담고 있다.
전자는 화면에 직접 표시되지는 않지만 브라우저가 페이지를 해석하고 표시하는 데 중요한 역할을 한다. 후자는 사용자가 실제로 보는 콘텐츠가 들어간다. 모든 UI가 여기 들어가는 것이다.
두 번째 줄은 문자 인코딩을 설정한다.
이는 컴퓨터가 문자를 숫자로 변환해 저장하고, 다시 숫자를 문자로 변환해 화면에 표시하는 방식이다. 예전에는 ASCII 인코딩이 주로 사용됐다. 하지만 영어 이외의 언어를 표현하는 데는 한계가 있었다.
반면 UTF-8 같은 인코딩 방식은 모든 문자를 표현할 수 있는 방식이다.
세 번째 줄은 브라우저 탭에 표시되는 제목을 설정한다.
네 번째 줄은 `style.css` 파일을 불러와 스타일을 적용한다.
`link` 태그는 HTML 문서에 외부 리소스를 연결할 때 사용한다. `rel="stylesheet"`를 통해 스타일 시트를 불러옴을 명시한다.
특별한 건 없고 selector에 대해 구별을 해보자.
`id`는 한 문서에서 딱 하나만 존재하는 고윳값이다.
`class`는 여러 요소에 부여해도 된다. 각종 스타일이나 동작을 일괄적으로 부여할 때 사용한다.
`name`은 폼 요소에서 데이터를 전송할 때 사용한다. 서버로 데이터가 전송될 때 key의 이름으로 쓰인다.
`for`은 `<label>`과 폼 요소를 연결할 때 사용한다. for에 설정된 값이 폼 요소의 id와 같아야 한다.
얘도 <body> 안에 포함된다.
`app.js`를 불러와서 JavaScript 로직을 실행한다.
💡 styles.css
못생긴 HTML 문서를 꾸며준다.
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
h1 {
color: #333;
}
form {
margin-bottom: 20px;
}
input {
padding: 10px;
width: 200px;
}
button {
padding: 10px;
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 10px;
border-bottom: 1px solid #ddd;
}
button {
margin-left: 10px;
}
주저리주저리 얘기할 건 없으니 자세한 설명은 생략.
💡 app.js
JavaScript로 작성된 프론트엔드 코드다. 우리가 만든 앱에서 UI를 제어하고 서버와 통신하는 역할을 한다.
document.addEventListener("DOMContentLoaded", () => {
const todoForm = document.getElementById("todoForm");
const taskInput = document.getElementById("taskInput");
const todoList = document.getElementById("todoList");
// To-Do 목록 불러오기
const loadTodos = async () => {
const response = await fetch("/api/todos");
const { todos } = await response.json();
todoList.innerHTML = "";
todos.forEach((todo) => {
const li = document.createElement("li");
li.textContent = todo.task;
li.dataset.id = todo.id;
const deleteButton = document.createElement("button");
deleteButton.textContent = "Delete";
deleteButton.onclick = async () => {
await fetch(`/api/todos/${todo.id}`, { method: "DELETE" });
loadTodos();
};
li.appendChild(deleteButton);
todoList.appendChild(li);
});
};
// 새로운 To-Do 추가
todoForm.onsubmit = async (e) => {
e.preventDefault();
const task = taskInput.value;
await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ task }),
});
taskInput.value = "";
loadTodos();
};
loadTodos();
});
첫 번째 줄이 없으면 페이지가 완전히 로드되기 전에 JavaScript 코드가 실행될 가능성이 있다. 그러면 해당 요소를 찾지 못해 오류가 발생한다.
다음으로 DOM 요소 선택을 하는데 `getElementById`를 통해 id 속성을 기반으로 해당 요소만 선택한다.
그리고 다음을 이해하기 전에 먼저 async와 await를 알아야 한다. 이는 JavaScript에서 비동기 작업을 처리할 때 사용되는 문법이다. 비동기 작업을 통해 하나의 작업에 대한 결과를 기다리는 동안 다른 작업을 진행할 수 있다.
함수 앞에 async를 선언하면 그 안에 await를 사용할 수 있다.
await를 쓰지 않으면 fetch가 완료되기 전에 바로 다음 줄로 넘어가서 아무것도 없는 result가 출력될 수 있는데 이걸 방지한다. await를 걸어놓으면 완료될 때까지 기다린다. (기다리는 동안 아무것도 안 하는 게 아니라 해당 async 함수 밖에 있는 다른 걸 실행한다.)
예시를 보면 다음과 같다.
이걸 실행하면,
`asyncFunction`을 진행하다가 await가 걸리면 밖으로 나와서 다른 거 먼저 실행한다. async 함수 내에서는 순차적으로 처리된다.
이건 기니까 쪼개서 볼 필요가 있다.
// To-Do 목록 불러오기
const loadTodos = async () => {
const response = await fetch("/api/todos");
const { todos } = await response.json();
첫 번째 줄에서 `loadTodos`라는 이름의 비동기 함수를 선언했다.
두 번째 줄에서 "/api/todos" 경로로 fetch 요청을 보내 여기에 저장된 데이터를 가져온다.
fetch에 별도로 `method`를 명시하지 않으면 해당 경로로 GET 요청을 보낸다. 물론, POST, PUT, DELETE 등 다른 걸 사용할 수도 있다.
저 경로는 백엔드의 `server.js` 파일에서 설정한 것이다. 클라이언트가 서버에 요청하는 API 엔드포인트(클라이언트와 서버가 데이터를 주고받는 특정 주소, 즉 약속이다)다. 여기에는 대략 이런 식으로 작성되어 있을 것이다.
const express = require("express");
const app = express();
const PORT = 3000;
// To-Do 목록 데이터를 반환하는 엔드포인트
app.get("/api/todos", (req, res) => {
// 모든 To-Do 데이터를 가져와서 응답
const todos = [
{ id: 1, task: "Do the dishes" },
{ id: 2, task: "Complete homework" },
{ id: 3, task: "Walk the dog" }
];
res.json({ todos });
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
`res.json({ todos })`를 통해 서버에서 클라이언트로 JSON 형식의 데이터를 응답한다.
`{ todos }`는 객체 리터럴로, {키 : 값} 형태의 데이터를 축약해서 표현할 때 사용된다. 실제로는 `{todos : todos}`가 되는 것이다. (key에는 저 이름 그대로 들어가고, value에는 값이 풀어져 들어간다)
그리고 res에서 JSON을 내보내긴 하지만 본질적으로 서버로부터 받은 객체는 원시적인 HTTP 응답 객체다. 서버와 클라이언트가 소통할 때는 HTTP 요청을 보내고 HTTP 응답 객체를 보낸다.
우리가 원하는 JSON을 포함해서 이것저것 들어가 있다. 따라서 이 중에서 JSON만 취해야 한다. 여기서는 응답 본문(body)가 JSON이기 때문에 이걸 취하기 위해 굳이 `response.json()`을 추가로 붙이는 것이다.
todoList.innerHTML = "";
기존 화면에 있는 todoList를 초기화한다. 데이터가 삭제되는 건 아니다. 데이터는 db에 잘 저장되어 있다. db에 있는 걸 매번 새로 들고 와서 보여줘야 하기 때문에 현재 화면에 보이는 것은 삭제하는 것이다.
todos.forEach((todo) => {
const li = document.createElement("li");
li.textContent = todo.task;
li.dataset.id = todo.id;
forEach문을 돌려서 todos에 있는 각 요소들을 todo로 나타낸다.
<li>요소를 만들고 task와 id의 값을 각각 저장한다.
`textContent`로는 해당 태그 안에 있는 내용을 직접 입력하고, `dataset.id`로는 <li> 요소에 `data-id` 속성을 추가한다. (dataset 다음에는 어떤 것이든 쓸 수 있다. dataset.newjeans라고 입력하면 HTML에는 data-newjeans라고 태그가 붙는다.)
예시는 다음과 같다.
li를 만들고 다음에는 button을 만들 거다. 삭제 버튼이다.
const deleteButton = document.createElement("button");
deleteButton.textContent = "Delete";
deleteButton.onclick = async () => {
await fetch(`/api/todos/${todo.id}`, { method: "DELETE" });
loadTodos();
};
`textContent`를 통해 글자를 입력한다.
`onclick`은 HTML 요소에 클릭 이벤트를 지정한다.
여기에는 async를 걸었다. 왜냐면 삭제된 다음에 loadTodos를 실행해야 하기 때문이다.
해당 id를 지닌 todo를 삭제하는 버튼이다.
li.appendChild(deleteButton);
todoList.appendChild(li);
});
마지막으로 이 버튼을 li에 추가하고 이 li를 todoList에 추가한다.
다시 전체 흐름을 살펴보자.
다음으로 새로운 To-Do를 추가하는 기능이다.
`onsubmit`을 통해 폼이 제출될 때 이 코드가 실행되도록 한다. 역시 async를 걸었다.
`preventDefault()`는 원래 브라우저는 폼을 제출하면 페이지를 새로고침하는데 이걸 막는다.
사용자가 입력한 값을 `task` 변수에 넣고,
이걸 JSON 문자열로 변환하여 요청 본문에 포함해 /api/todos 경로에 POST 요청을 보낸다.
이후 taskInput의 입력 내용을 비우고, loadTodos()를 한다.
마지막으로 이건 페이지가 처음 로드될 때를 위해 표시해둔다.
다음 글에서는 백엔드를 뜯어보자!
'분명 전산학부 졸업 했는데 코딩 개못하는 조준호 > Web' 카테고리의 다른 글
한국은행 들어갈 때까지만 합니다
조만간 티비에서 봅시다