์๋ฐ์คํฌ๋ฆฝํธ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๊ฐ๋จํ ํฌ์คํ ํ๋ ค๊ณ ์ฌํํ To Do List๋ฅผ ๋ง๋ค์ด ๋ณด์๋ค!
1. ๊ตฌ๊ธํฐํธ, ์คํ์ผ์ํธ, ์๋ฐ์คํฌ๋ฆฝํธ ์ฐ๊ฒฐ
- `style.css`์ `script.js` ์ฐ๊ฒฐ
- ๊ตฌ๊ธ ํฐํธ `Poppins` ์ฌ์ฉํ๊ธฐ ์ํด์ Embed Code๋ฅผ <head> ๋ถ๋ถ์ ๋ฃ์ด ์ฃผ์์.
- <body> ํ๊ทธ ์์ <div class="container"></div> ์ถ๊ฐ
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
</div>
</body>
<script src="script.js"></script>
</html>
2. ์ ์ฒด ์ ์ฉ๋ css ์ค์ & container์ ๋ฐฐ๊ฒฝ ์ถ๊ฐ
โก ํฐํธ๋ ๊ตฌ๊ธ ํฐํธ `Poppins` ๋ก ์ค์
โก `box-sizing: border-box;`
์์์ ํฌ๊ธฐ๋ฅผ ๊ณ์ฐํ ๋ `ํจ๋ฉ(padding)`๊ณผ `ํ ๋๋ฆฌ(border)`๊น์ง ํฌํจํ๋๋ก ์ค์ ํ๋ ์์ฑ. ๊ธฐ๋ณธ์ ์ผ๋ก CSS์์ box-sizing์ ๊ธฐ๋ณธ๊ฐ์ `content-box`์ด๋ค. `content-box`๋ ์์์ `width`์ `height`๊ฐ ์ฝํ ์ธ ์์ญ๋ง์ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐ๋๋ค. ํจ๋ฉ๊ณผ ํ ๋๋ฆฌ๋ ๋ณ๋์ด๋ค.
โก ์ปจํ ์ด๋ ์ต์ ๋์ด๋ ๋ทฐํฌํธ ๋์ด 100%๋ก ์ค์ (ํ๋ฉด ์ ์ฒด ๋์ด๋ก ์ค์ ํด ์ค)
* {
margin: 0;
padding: 0;
font-family: "Poppins", sans-serif;
box-sizing: border-box ;
}
.container {
width: 100%;
min-height: 100vh;
background: linear-gradient(135deg, #153677, #4e085f);
padding: 10px;
}
3. To Do List ์์ญ
์ปจํ ์ด๋์ ์์ To Do List ์์ญ์ ์ก์์ค๋ค.
h2 ํ๊ทธ ์์ To Do List ์ ๋ ฅํ๊ณ ๋ ธํธ ์ด๋ฏธ์ง๋ฅผ ๋ฃ์ด์ฃผ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋์ To Do List ์์ญ์ flex ์ค์ ์ ํด ์ฃผ๊ณ , ์ปจํ ์ธ ๋ฅผ ์ค์ ๋ฐฐ์น ํด ์ฃผ์๋ค.
๋ค๋ฅธ ์์ฑ๋ค์ ์ฌ์ฐ๋๊น ๊ฐ๋จํ `flex` ์์ฑ์ธ `align-item`๊ณผ `justify-content` ์ฐจ์ด๋ง ์ ์ด ๋ด
ํ์ฌ ํด๋์ค๊ฐ ๊ฐ์ง ์์ ์์๋ฅผ ์ ๋ ฌํ๋ ์์ฑ์ด๋ค.
โก `align-items` → ์์ง ์ ๋ ฌ ๊ธฐ๋ณธ(์ธ๋ก๋ฐฉํฅ)
โก `justify-content` → ์ํ ์ ๋ ฌ ๊ธฐ๋ณธ(๊ฐ๋ก๋ฐฉํฅ)
flex-direction์ ๋ฐ๋ผ ์ถ ๋ฐฉํฅ์ด ๋ณ๊ฒฝ๋๋ค.
โก `justify-content: space-between` → ์์ ์์๋ค์ด ์ฃผ ์ถ์ ๋ฐ๋ผ ์์ชฝ์ ๊ท ๋ฑํ๊ฒ ๋ฐฐ์น
<div class="container">
<div class="todo-app">
<h2>To Do List<img src="images/icon.png"></h2>
</div>
</div>
.todo-app {
width: 100%;
max-width: 540px;
background: #fff;
margin: 100px auto 20px;
padding: 40px 30px 70px;
border-radius: 10px;
}
.todo-app h2 {
color: #002765;
display: flex;
align-items: center;
margin-bottom: 20px;
}
.todo-app h2 img {
width: 30px;
margin-left: 10px;
}
4. ํ ์ผ์ ์ ๋ ฅ๋ฐ์ <input> ํ๊ทธ์ add ๋ฒํผ ๋ง๋ค๊ธฐ
.row ์์ญ์ `input ํ๊ทธ`์ `Add ๋ฒํผ` ์ถ๊ฐ
โก `flex: 1`
Flexbox ์ปจํ
์ด๋ ๋ด์์ input ์์๊ฐ ๋จ์ ๊ณต๊ฐ์ ๊ท ๋ฑํ๊ฒ ์ฐจ์งํ๋๋ก ์ค์ ํ๋ ๊ฒ. ์ฝ๊ฒ ๋งํ๋ฉด ํ๋ ์ค ๋ฐ์ค ์์์ ์์๊ฐ ๊ณต๊ฐ์ `1/n`ํ๋ ๊ฒ์ด๋ค. ๋ง์ฝ ์์๊ฐ 3๊ฐ๋ฉด ๊ฐ๊ฐ ๋จ๋ ๊ณต๊ฐ์ 1/3์ฉ ์ฐจ์งํ๋ค.
โก `border: none;`
input ์์์ ํ ๋๋ฆฌ ์ ๊ฑฐ, ๊น๋ํ๊ณ ๋ฏธ๋๋ฉํ ์คํ์ผ์ ์ํ ๋ ์ฌ์ฉ ํด ์ค๋ค.
โก `outline: none;`
input์ด ํฌ์ปค์ค ์ํ์ผ ๋ ๋ํ๋๋๊ธฐ๋ณธ ์์๋ผ์ธ(์ธ๊ณฝ์ ) ์ ๊ฑฐ
์๋ ์น์ฌ์ดํธ์์๋ ์ฌ์ฉ์๊ฐ ํน์ ๋ฒํผ์ ํฌ์ปค์คํ๋ฉด ์๊ฐ์ ํํธ๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด์ ์์๋ผ์ธ์ ๋จนํ ์ฃผ๋๊ฒ ์ผ๋ฐ์ ์ธ๋ฐ ์ฌ๊ธฐ์๋ ๊น๊ธํ๊ฒ ํ๋ ค๊ณ ์ ๊ฑฐํ๋ค.
โก `placeholder`
์ด๋ค ๋ด์ฉ์ ์ ๋ ฅํด์ผ ํ๋์ง ํํธ๋ฅผ ์ฃผ๋ ์์ฑ
<body>
<div class="container">
<div class="todo-app">
<h2>To Do List<img src="images/icon.png"></h2>
<div class="row">
<input type="text" id="input-box" placeholder="Add your task">
<button>Add</button>
</div>
</div>
</div>
</body>
.row {
display: flex;
align-items: center;
justify-content: space-between;
background: #edeef0;
border-radius: 30px;
padding-left: 20px;
margin-bottom: 25px;
}
input {
flex: 1;
border: none;
outline: none;
background: transparent;
padding: 10px;
}
button {
border: none;
outline: none;
padding: 16px 50px;
background: #ff5945;
color: #fff;
font-size: 16px;
cursor: pointer;
border-radius: 40px;
}
5. ul ํ๊ทธ๋ก Task ๋ชฉ๋ก ๋ง๋ค๊ธฐ
โก`<ul>` ํ๊ทธ
`Unordered List`๋ก HTML์์ ์์๊ฐ ์๋ ๋ฆฌ์คํธ๋ฅผ ์ ์ํ๋ ํ๊ทธ์ด๋ค ๋ณดํต ์ (bullet)์ผ๋ก ๊ฐ ํญ๋ชฉ์ด ํ์๋๋ค.
์์๊ฐ ํ์ํ ๋ฆฌ์คํธ๋ `<ol>` ํ๊ทธ๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์ซ์ ๋๋ ์ํ๋ฒณ ๋ฑ์ผ๋ก ํ์ํ๋ค.
โก `list-style: none;`๋ก ulํ๊ทธ์ ์ ๋ง์ปค๋ฅผ ๊ฐ๋ ค ์ฃผ ์๋ค.
โก `user-select: none;`
ํ ์คํธ ๋ณต์ฌ ๋ฐฉ์ง
โก `::before`
๊ฐ์์ ํ์๋ก ์ ํ ์์๊ฐ ์ ํ๋์๋์ง ์๋์ง ๋ณด์ฌ์ฃผ๋ ์ฒดํฌ ๋ง์ปค๋ฅผ ๋ง๋ค์ด์ฃผ์๋ค.
โก `border-radius: 50%`
border-radius ์์ฑ์ 50%๋ก ๋จน์ด์ฃผ๋ฉด ์ ์ฌ๊ฐํ๊ธฐ์ค์ผ๋ก ์์ ํ ์์ผ๋ก ํ ๋๋ฆฌ๊ฐ ๊น์ธ๋ค. ์ง์ฌ๊ฐํ์๋ ์ถ์ฒํ์ง ์๋๋ค~ ๊ถ๊ธํ์ ๋ถ์ ์ด๋ป๊ฒ ๋๋์ง ํด๋ณด์๊ธธ!
โก `position: relative;`๋ ์์ ์์์ ์์น๋ฅผ ์๋์ ์ผ๋ก ์กฐ์ ํ ์ ์๊ฒ ํด์ค๋ค.
โก `position: absolute;`๋ ์์ ์์์ ์์น๋ฅผ ๋ถ๋ชจ ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ์ ํํ๊ฒ ์ค์ ํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
<body>
<div class="container">
<div class="todo-app">
<h2>To Do List<img src="images/icon.png"></h2>
<div class="row">
<input type="text" id="input-box" placeholder="Add your task">
<button>Add</button>
</div>
<ul id="list-container">
<li>Task 1</li>
<li>Task 2</li>
<li>Task 3</li>
</ul>
</div>
</div>
</body>
ul li {
list-style: none;
font-size: 17px;
padding: 12px 8px 12px 50px;
user-select: none;
cursor: pointer;
position: relative;
}
ul li::before {
content: '';
position: absolute;
height: 28px;
width: 28px;
border-radius: 50px;
background-image: url(images/unchecked.png);
background-size: cover;
background-position: center;
top: 12px;
left: 8px;
}
์๋ฃํ ํ ์ผ์ ๊ฐ๋ก์ ๊ธ๊ธฐ
๋ฆฌ์คํธ์ ์ฒซ๋ฒ์งธ ์์ดํ ์ checked ํด๋์ค๋ฅผ ์ถ๊ฐํจ
`.checked` ๊ฐ ์ค์ ๋ ๋ฆฌ์คํธ๋
โก `text-decoration: line-through;` ์์ฑ์ผ๋ก ์ค์์ ๊ฐ๋ก ์ค์ ๊ทธ์ด์ฃผ์์!
<ul id="list-container">
<li class="checked">Task 1</li>
<li>Task 2</li>
<li>Task 3</li>
</ul>
ul li.checked {
color: #555;
text-decoration: line-through;
}
์๋ฃํ ๋ฆฌ์คํธ์ ์ฒดํฌ ์์ด์ฝ ํ์ํ๊ธฐ
๊ฐ์์ ํ์๋ก `.checked` ํด๋์ค๊ฐ ์ถ๊ฐ๋ ๋ฆฌ์คํธ๋ ์ฒดํฌ ํ์๋ฅผ ํด ์ค!
ul li.checked::before {
background-image: url(images/checked.png);
}
์๋ฃ๋ ํ ์ผ์ ์ญ์ ํ๋ ๋ฒํผ
์ด ๋ฒํผ์ `li` ์์ `span`ํ๊ทธ๋ฅผ ์์ฑํด์ ๋ง๋ค์ด ์ฃผ์๋ค.
์๋๋ ๋ฐฐ๊ฒฝ์ด ์๋ค๊ฐ ๋ง์ฐ์ค๋ฅผ ์ฌ๋ฆฌ๋ฉด ๋ฐฐ๊ฒฝ์์ด ๋ํ๋ ์ ์๋๋ก :hover ๋ก `background`๋ฅผ ์ง์ ํด ์ฃผ์๋ค.
ul li span {
position: absolute;
right: 0;
top: 5px;
width: 40px;
height: 40px;
font-size: 22px;
color: #555;
line-height: 40px;
text-align: center;
border-radius: 50%;
}
ul li span:hover {
background: #edeef0;
}
โ ์๋ฐ์คํฌ๋ฆฝํธ
const inputBox = document.getElementById("input-box");
const listContainer = document.getElementById("list-container");
`inputBox`์ `listContainer`๋ HTML์์ id="input-box"์ id="list-container"์ธ ์์๋ฅผ ์ฐธ์กฐํ๋ค.
// ํ ์ผ์ ์ถ ๊ฐํ๋ addTask() ํจ์
function addTask() {
if(inputBox.value === '') {
alert("You must write something!");
} else {
let li = document.createElement("li"); // ์๋ก์ด <li> ์์ ์์ฑ
li.innerHTML = inputBox.value; // inputBox์ ์
๋ ฅ๋ ๊ฐ์ผ๋ก <li>์ ๋ด์ฉ ์ค์
listContainer.appendChild(li); // <li> ์์๋ฅผ listContainer์ ์ถ๊ฐ
let span = document.createElement("span"); // ์๋ก์ด <span> ์์ ์์ฑ
span.innerHTML = "\u00d7"; // '×' ๋ฌธ์ ์ถ๊ฐ
li.appendChild(span); // <li> ์์์ <span> ์ถ๊ฐ
}
inputBox.value = ""; // ์
๋ ฅ์ฐฝ ์ด๊ธฐํ
saveData(); // ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ
}
addTask() ํจ์๋ ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๋ด์ฉ์ <li>๋ก ์ถ๊ฐํ๊ณ , ํ ์ผ ๋ชฉ๋ก์ '×' ๋ฒํผ์ ๋ง๋ค์ด ์ญ์ ํ๋ค.
์ ๋ ฅ์ฐฝ์ด ๋น์ด ์์ผ๋ฉด ๊ฒฝ๊ณ ๋ฉ์์ง๋ฅผ ๋์ด๋ค.
โก `span.innerHTML = "\u00d7";` ์์ `\u00d7`๋ ์ ๋์ฝ๋ ๋ฌธ์๋ก, `× ๊ธฐํธ`๋ฅผ ๋ํ๋ธ๋ค.
`\u00d7`๋ ์ ๋์ฝ๋ `00D7`์ ํด๋นํ๋ ๋ฌธ์๋ผ๋ ๋ป์ด๊ณ `๊ณฑ์ ๊ธฐํธ` or `๋ซ๊ธฐ(X) ๋ฒํผ`์ผ๋ก ์์ฃผ ์ฌ์ฉ๋๋ค.
// ํด๋ฆญ ์ด๋ฒคํธ
listContainer.addEventListener("click", function(e){
// ํด๋ฆญ๋ ์์๊ฐ li ํ๊ทธ์ด๋ฉด
if(e.target.tagName === "LI"){ // ํด๋ฆญ๋ ์์๊ฐ <li>์ผ ๊ฒฝ์ฐ
e.target.classList.toggle("checked"); // 'checked' ํด๋์ค๋ฅผ ํ ๊ธํ์ฌ ์๋ฃ๋ ํญ๋ชฉ ํ์
saveData(); // ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ
}
// ํด๋ฆญ๋ ์์๊ฐ span ํ๊ทธ์ด๋ฉด
else if(e.target.tagName === "SPAN"){ // ํด๋ฆญ๋ ์์๊ฐ <span>์ผ ๊ฒฝ์ฐ (์ญ์ ๋ฒํผ)
e.target.parentElement.remove(); // ํด๋น <li> ์์ ์ญ์
saveData(); // ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ
}
}, false);
ํด๋ฆญํ ํญ๋ชฉ์ ๋ฐ๋ผ ํ ์ผ ์๋ฃ ํ์ (checked ํด๋์ค ํ ๊ธ) or ์ญ์ ์์ ์ ํ๋ ํจ์์ด๋ค.
ํด๋ฆญ๋ ํญ๋ชฉ์ด <span>์ผ ๊ฒฝ์ฐ์๋ ํด๋น ํญ๋ชฉ์ ์ญ์ ํ๋ค.
// ํ ์ผ ๋ชฉ๋ก์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅํ๋ ํจ์
function saveData(){
localStorage.setItem("data", listContainer.innerHTML);
}
// ๋ก์ปฌ ์คํ ๋ฆฌ์ง์์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ ๋ชฉ๋ก์ ํ์
function showTask(){
listContainer.innerHTML = localStorage.getItem("data");
}
showTask(); // ํ์ด์ง๊ฐ ๋ก๋๋ ๋ ๋ชฉ๋ก ํ์
โก `saveData()` ํจ์๋ ํ์ฌ ํ ์ผ ๋ชฉ๋ก์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅํด์, ํ์ด์ง๊ฐ ์๋ก๊ณ ์นจ๋๊ฑฐ๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์๋ค๊ฐ ๋ค์ ์ด์ด๋ ๋ชฉ๋ก์ด ์ ์ง๋๋๋ก ํ๋ค.
โก showTask() ํจ์๋ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅ๋ ํ ์ผ ๋ชฉ๋ก์ ๋ถ๋ฌ์ ํ๋ฉด์ ํ์ํ๋ค. ํ์ด์ง๊ฐ ๋ก๋๋ ๋ ์๋์ผ๋ก ์คํ๋๋ค.
โ ์ ์ฒด์ฝ๋
[html ์ฝ๋]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="todo-app">
<h2>To Do List<img src="images/icon.png"></h2>
<div class="row">
<input type="text" id="input-box" placeholder="Add your task">
<button onclick="addTask()">Add</button>
</div>
<ul id="list-container">
<li class="checked">Task 1</li>
<li>Task 2</li>
<li>Task 3</li>
</ul>
</div>
</div>
</body>
<script src="script.js"></script>
</html>
[css ์ฝ๋]
* {
margin: 0;
padding: 0;
font-family: "Poppins", sans-serif;
box-sizing: border-box ;
}
.container {
width: 100%;
min-height: 100vh;
background: linear-gradient(135deg, #153677, #4e085f);
padding: 10px;
}
.todo-app {
width: 100%;
max-width: 540px;
background: #fff;
margin: 100px auto 20px;
padding: 40px 30px 70px;
border-radius: 10px;
}
.todo-app h2 {
color: #002765;
display: flex;
align-items: center;
margin-bottom: 20px;
}
.todo-app h2 img {
width: 30px;
margin-left: 10px;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
background: #edeef0;
border-radius: 30px;
padding-left: 20px;
margin-bottom: 25px;
}
input {
flex: 1;
border: none;
outline: none;
background: transparent;
padding: 10px;
}
button {
border: none;
outline: none;
padding: 16px 50px;
background: #ff5945;
color: #fff;
font-size: 16px;
cursor: pointer;
border-radius: 40px;
}
ul li {
list-style: none;
font-size: 17px;
padding: 12px 8px 12px 50px;
user-select: none;
cursor: pointer;
position: relative;
}
ul li::before {
content: '';
position: absolute;
height: 28px;
width: 28px;
border-radius: 50px;
background-image: url(images/unchecked.png);
background-size: cover;
background-position: center;
top: 12px;
left: 8px;
}
ul li.checked {
color: #555;
text-decoration: line-through;
}
ul li.checked::before {
background-image: url(images/checked.png);
}
ul li span {
position: absolute;
right: 0;
top: 5px;
width: 40px;
height: 40px;
font-size: 22px;
color: #555;
line-height: 40px;
text-align: center;
border-radius: 50%;
}
ul li span:hover {
background: #edeef0;
}
[js ์ฝ๋]
const inputBox = document.getElementById("input-box");
const listContainer = document.getElementById("list-container");
function addTask() {
if(inputBox.value === '') {
alert("You must write something!");
} else {
let li = document.createElement("li");
li.innerHTML = inputBox.value;
listContainer.appendChild(li);
let span = document.createElement("span");
span.innerHTML = "\u00d7";
li.appendChild(span);
}
inputBox.value = "";
saveData();
}
listContainer.addEventListener("click", function(e){
if(e.target.tagName === "LI"){
e.target.classList.toggle("checked");
saveData();
}
else if(e.target.tagName === "SPAN"){
e.target.parentElement.remove();
saveData();
}
}, false);
function saveData(){
localStorage.setItem("data", listContainer.innerHTML);
}
function showTask(){
listContainer.innerHTML = localStorage.getItem("data");
}
showTask();
[์ด๋ฏธ์งํ์ผ]