유튜버 '노마드 코더'님의 무료강의 중 모멘텀 앱을 따라 만들어 보았다.
니꼬쌤의 강의는 이 강의로 처음 접해보았다.
자막이 잘 달려있음에도 베이스가 영어로 된 강의라 초반에는 버벅거렸는데 금방 익숙해졌다.
기초에 대한 개념을 천천히, 이해하기 쉽게 잘 설명해줘서 좋았다.
나중에 유료강의도 들어봐야겠다.
일하면서 중간중간 시간날 때마다 작업을 했다.
에디터는 repl.it을 이용
앱의 주요 기능
- 새로고침시 배경화면 이미지 랜덤 변경
- 인풋창에 사용자 이름 입력시, [ Hello, {입력값} ] 출력 -- 로컬저장소 이용, 새로고침시 값 유지
- 우측 상단 시계 매초마다 자동갱신
- 좌측 상단 사용자 위치 기반 날씨정보 30초마다 자동갱신
- 새로고침시 중앙의 명언 랜덤 변경
- 하단 투두리스트(Todo list) -- 로컬저장소 이용, 새로고침시 값 유지
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>replit</title>
<link href="CSS/style.css" rel="stylesheet" type="text/css" />
<!-- Loads the Extensions API -->
<script src="https://unpkg.com/@replit/extensions@0.24.0/dist/index.global.js"></script>
</head>
<body>
<!-- 메인박스 -->
<div id="main_container">
<!-- 로그인 -->
<form id="login_container">
<input
type="text"
id="login_input"
placeholder="What is your name?"
required
>
<button id="login_button">Submit</button>
</form>
<!-- 인사 -->
<h3 id="greeting"></h3>
<!-- 시계 -->
<div id="clock_container">
11:30 AM
</div>
<!-- 명언 -->
<div id="quotes_container">
<p class="quotes_quote">asdasd</p>
<p class="quotes_author">123123</p>
</div>
<!-- 투두리스트 -->
<div id="todo_container">
<form class="todo_form">
<input
type="text"
id="todo_input"
placeholder="What will you do today?"
required
>
<button id="todo_button">Submit</button>
</form>
<ul id="list_container">
</ul>
</div>
<!-- 날씨 -->
<div id="weather_container">
<p class="weather_location"></p>
<p class="weather_temp"></p>
<p class="weather_weather"></p>
</div>
</div>
<!-- JS 파일 불러오기 -->
<script src="script.js"></script>
<script src="JS/bg.js"></script>
<script src="JS/login.js"></script>
<script src="JS/clock.js"></script>
<script src="JS/quotes.js"></script>
<script src="JS/todo.js"></script>
<script src="JS/weather.js"></script>
</body>
</html>
HTML 코드
딱히 복잡한 부분은 없다.
하나 기억나는게 있다면 input 태그는 속성값이 어마어마하게 많다는 거 정도.
/* 스타일 리셋 */
@import 'reset.css';
/* 커스텀 디자인 */
:root {
--color-white : #FFF;
--color-violet : #5b2386;
--color-yellow : #F5DF4D;
--color-grey : #BABCBE;
--font-family-Noto : 'Noto Sans KR';
--font-family-Enjoy : 'OTEnjoystoriesBA';
--font-family-G-Medium : 'GmarketSansMedium';
--font-family-G-Bold : 'GmarketSansBold';
--animation-duration : 300ms;
}
/* 폰트 (4종류) */
@font-face {
font-family: 'OTEnjoystoriesBA';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_two@1.0/OTEnjoystoriesBA.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'GmarketSansBold';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2001@1.1/GmarketSansBold.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'GmarketSansMedium';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2001@1.1/GmarketSansMedium.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 400;
src: url("https://fonts.gstatic.com/ea/notosanskr/v2/NotoSansKR-Regular.woff2") format('woff2'),
url("https://fonts.gstatic.com/ea/notosanskr/v2/NotoSansKR-Regular.woff") format('woff'),
url("https://fonts.gstatic.com/ea/notosanskr/v2/NotoSansKR-Regular.otf") format('opentype');
}
html {
font-size : 16px;
}
button, input {
border : none;
border-radius: 8px;
outline : none;
padding: 8px 12px;
cursor : pointer;
margin-left: .5rem;
}
button {
background-color: var(--color-yellow);
color: var(--color-violet);
transition: var(--animation-duration) opacity;
}
button:hover {
opacity: .77;
}
.hidden {
display: none;
}
/* 메인 박스 + 배경화면 */
#main_container {
position: relative;
font-family: 'GmarketSansMedium';
color : #fff;
width: 100%;
height: 100vh;
background-size: cover;
background-position: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 10rem 0;
}
/* 로그인 */
#login_container {
margin: 2rem 0;
}
#login_input, #login_button {
font-size : 1.25rem;
}
#greeting {
font-size : 3.5rem;
color : var(--color-yellow);
font-weight: 700;
margin: 2rem 0;
}
/* 시계 */
#clock_container {
position: absolute;
top : 2rem;
right : 2rem;
font-size : 1.75rem;
color : var(--color-yellow);
font-weight: 700;
opacity: .77;
}
/* 명언 */
#quotes_container {
display: flex;
flex-direction: column;
justify-content: center;
margin: 2rem 10rem;
}
.quotes_quote {
margin-bottom: 1rem;
font-size: 1.25rem;
color: var(--color-white);
}
.quotes_author {
color: var(--color-yellow);
}
/* 투두리스트 */
#todo_container {
margin: 2rem 0;
}
.todo_form {
margin-bottom: 2rem;
}
#todo_input, #todo_button {
font-size: 1.25rem;
}
#list_container {
max-height : 170px;
overflow-y: scroll;
}
#list_container li {
background-color: var(--color-white);
color: var(--color-violet);
padding: 8px 12px;
margin: 8px 12px;
border-radius: 8px;
line-height: 1.5;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 날씨 */
#weather_container {
position: absolute;
top : 2rem;
left : 2rem;
font-size : 1.75rem;
color : var(--color-yellow);
font-weight: 700;
opacity: .77;
}
CSS 코드
특별히 어려운 점은 없었다.
지금 업로드 하면서 눈치챈 거지만 font를 4개나 받아와 놓고 그중 하나만 사용했다는 거.
복사 붙여 넣기를 해서 눈치채지 못했다. 나중에 나머지 세 개는 지워야겠다.
const container = document.getElementById('main_container');
// 이미지 파일 배열
const imgArr = [
'1.jpg',
'2.jpg',
'3.jpg',
'4.jpg',
'5.jpg',
'6.jpg',
'7.jpg',
'8.jpg',
'9.jpg',
'10.jpg',
];
// 랜덤 이미지 파일 받기
function getRandomElement(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// 배경화면에 랜덤 이미지 삽입
const randomImage = getRandomElement(imgArr);
container.style.backgroundImage = `url('Images/${randomImage}')`;
JS 파일 1/6 배경화면 변경 관련 코드
10개의 이미지 파일을 Images 폴더 안에 넣어둔 후 각각의 파일명을 배열에 하드코딩해서 넣어놨다.
그 후에 랜덤수를 배열의 배수만큼 곱한 후 소숫점 자리를 모두 버리면 0~9까지의 랜덤수를 뽑아낼 수 있다.
그 후엔 style.backgroundImage 속성으로 직접 이미지를 삽입한다.
크게 어려운 부분은 없었다.
const clock = document.getElementById('clock_container');
function getCurrentTime() {
const currentDate = new Date();
const currentTimeString = currentDate.toLocaleTimeString("ko-KR");
clock.innerText = currentTimeString;
}
getCurrentTime();
const currentTime = setInterval(getCurrentTime, 1000);
JS 파일 2/6 디지털 시계 관련 코드
new Date() 를 이용해 현재 날짜와 관련된 값을 받아온 후, toLocaleTimeString("ko-KR") 메서드를 이용.
한국 기준 시간을 불러왔다.
강의에서는. getHours , getMinutes 등의 메서드를 이용해서 각각의 시, 분, 초 값을 받는 방식을 택했다.
난 귀찮아서 toLocaleTimeString 메서드를 이용했다.
값을 잘 받았으면 매 초(1000ms)마다 값을 갱신할 수 있도록 setInterval() 함수를 이용해 준다.
setTimeout과는 달리 지정한 시간마다 원하는 값을 갱신할 수 있도록 도와주는 똑똑한 녀석이다.
여기도 크게 어려운 부분은 없었다.
const loginForm = document.getElementById('login_container');
const loginInput = document.querySelector('#login_input');
const greeting = document.getElementById('greeting');
// 로컬 저장소 키값
const USER_KEY = 'userName';
const HIDDEN_CLASS = 'hidden';
// 인풋값을 받아 로컬 저장소에 저장 + 디스플레이 효과
function getLoginValue() {
const username = loginInput.value;
loginForm.classList.add(HIDDEN_CLASS);
greeting.classList.remove(HIDDEN_CLASS);
greeting.innerText = 'Hello, ' + username;
localStorage.setItem(USER_KEY, username);
}
// 서브밋 핸들러
function submitLogin(e) {
e.preventDefault();
getLoginValue();
}
loginForm.addEventListener('submit', submitLogin);
// 로컬 저장소의 유저명
const localUserName = localStorage.getItem(USER_KEY);
// 새로고침 시 오류방지
if(localUserName) {
loginForm.classList.add(HIDDEN_CLASS);
greeting.innerText = 'Hello, ' + localUserName;
greeting.classList.remove(HIDDEN_CLASS);
} else {
loginForm.classList.remove(HIDDEN_CLASS);
greeting.classList.add(HIDDEN_CLASS);
}
JS 파일 3/6 로그인 관련 코드
원하는 값을 브라우저에 저장해 주는 기능인 localStorage (그 외 sessionStorage도 있음).
이 녀석은 몇 번을 해도 어렵다.. 이 코드같이 value가 하나인 경우는 그나마 해보겠는데 배열이나 오브젝트가 되면 너무너무 복잡해지는 것 같다.
코드 작동 순서
- 사용자가 인풋값을 제출하면
- e.preventDefault()를 이용해 새로고침을 방지
- 사용자가 제출한 값을 받아서 greeting에 삽입
- loginForm은 안 보이게, greeting은 보이게 설정
- 사용자가 제출한 값 localStorage에 저장
- 마지막으로 if, else 문으로 새로고침 방지
코드가 복잡해지면 오타도 덩달아 문제인 것 같다. 오타 때문에 날린 시간도 꽤 됐기에 조심 또 조심해야겠다.
const quote = document.querySelector('.quotes_quote');
const author = document.querySelector('.quotes_author');
// 명언 배열
const quotes = [
{
quote: 'I never dreamed about success, I worked for it',
author: 'Estee Lauder'
},
{
quote: 'Do not try to be original, just try to be good.',
author: 'Paul Rand'
},
{
quote: 'Do not be afraid to give up the good to go for the great',
author: 'John D. Rockefeller'
},
{
quote: 'If you cannot fly then run. If you cannot run, then walk. And if you cannot walk, then crawl, but whatever you do, you have to keep moving forward.',
author: 'Martin Luther King Jr.'
},
{
quote: 'Our greatest weakness lies in giving up. The most certain way to succeed is always to try just one more time.',
author: 'Thomas Edison'
},
{
quote: 'The fastest way to change yourself is to hang out with people who are already the way you want to be',
author: 'REid Hoffman'
},
{
quote: 'Money is like gasoline during a road trip. You do not want to run out of gas on your trip, but you are not doing a tour of gas stations',
author: 'Tim O Reilly'
},
{
quote: 'Some people dream of success, while other people get up every morning and make it happen',
author: 'Wayne Huizenga'
},
{
quote: 'The only thing worse than starting something and falling.. is not starting something',
author: 'SEth Godin'
},
{
quote: 'If you really want to do something, you will find a way. If you do not, you will find an excuse.',
author: 'Jim Rohn'
},
];
// 랜덤 수 불러오기 (최대값 : 배열의 길이)
let randomNum = Math.floor(Math.random() * quotes.length);
// 배열의 랜덤 수 번째 요소를 삽입
quote.innerText = quotes[randomNum].quote;
author.innerText = quotes[randomNum].author;
JS 파일 4/6 명언 관련 코드
감사하게도 강의 댓글에 어떤 수강자분이 명언 배열을 공유해 주셨다.
랜덤 수 로직은 배경화면 변경하는 로직과 다를 바 없었기에 쉬웠다.
'use strict';
const todoForm = document.querySelector('.todo_form');
const todoInput = document.querySelector('#todo_input');
const listBox = document.querySelector('#list_container');
const TODO_KEY = 'todos';
// 로컬스토리지에서 할 일 리스트 불러오기
let todos = JSON.parse(localStorage.getItem(TODO_KEY)) || [];
// 할 일 삭제 후 로컬스토리지에 저장하기
function saveTodos() {
localStorage.setItem(TODO_KEY, JSON.stringify(todos));
}
// 할 일 삭제하기
function deleteTodo (index) {
todos.splice(index, 1);
saveTodos();
}
// 할 일 삭제 버튼 핸들러
function handleDeleteBtn (e) {
const list = e.target.parentNode;
const index = todos.findIndex(todo => todo.id === list.id);
deleteTodo (index);
list.remove();
}
// 할 일 생성하기
function createTodo (todoValue) {
const listItem = document.createElement('li');
const listValue = document.createElement('span');
const deleteBtn = document.createElement('button');
const id = Date.now().toString();
listValue.textContent = todoValue;
deleteBtn.textContent = '🗑️';
deleteBtn.addEventListener('click', handleDeleteBtn);
listItem.appendChild(listValue);
listItem.appendChild(deleteBtn);
listItem.id = id;
listBox.appendChild(listItem);
const newTodo = { id, value: todoValue };
todos.push(newTodo);
saveTodos();
}
// 서브밋 핸들러
function handleTodoSubmit (e) {
e.preventDefault();
const todoValue = todoInput.value.trim();
if (todoValue === '') return;
createTodo (todoValue);
todoInput.value = '';
todoInput.focus();
}
// 초기 할 일 목록 불러오기
function loadTodos () {
todos.forEach(todo => createTodo (todo.value));
}
// 이벤트 리스너
todoForm.addEventListener('submit', handleTodoSubmit);
// 새로고침 방지
if (todos.length !== 0) {
loadTodos();
}
JS 파일 5/6 투두리스트(Todo list) 관련 코드
이번 프로젝트에서 가장 어려웠던 부분이다. (앞서 말했던 localStorage 관련해서 시간을 엄청 잡아먹었다.)
특히 삭제버튼 클릭 시 로컬스토리지 아이템도 함께 제거하는 작업이 어려웠다.
- 각 요소에 id값을 할당하고
- findIndex 함수로 해당 요소의 인덱스 값을 받은 후
- splice 함수로 index 번째 아이템을 1개 삭제
아직도 눈이 어질어질하다... ㅋㅋ
코드 작동 순서는 글로 표현하자니 머리가 지끈지끈해서 생략하겠다.
const weatherLocation = document.querySelector('.weather_location');
const temp = document.querySelector('.weather_temp');
const weather = document.querySelector('.weather_weather');
// API 데이터 불러오기
function callDatas(lat, lon) {
const API_KEY = 'your key'
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`;
fetch(url)
.then(response => {
if (response.ok) {
console.log(response);
return response.json();
}
throw new Error('Network response was not ok.');
})
.then(data => {
// 불러온 데이터를 처리하는 코드 작성
weatherLocation.innerText = data.name;
temp.innerText = `${Math.floor(data.main.temp)} °C`;
weather.innerText = data.weather[0].main;
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
});
}
// 사용자의 위도, 경도값 받아오기
function success(pos) {
const crd = pos.coords;
const lat = crd.latitude;
const lon = crd.longitude;
// 데이터가 있을 때 한 번 호출
// callDatas(lat, lon);
// 이후 30초마다 호출
setInterval(() => {
callDatas(lat,lon);
}, 30000);
}
function error(err) {
console.warn(`ERROR(${err.code}): ${err.message}`);
}
navigator.geolocation.getCurrentPosition(success, error);
JS 파일 6/6 날씨 관련 코드
사용자의 위도와 경도를 알아오는 것부터, fetch() 함수를 이용해서 openweathermap의 API 정보를 받아오는 것까지 쉽지만은 않았다.
참고로 사용자의 위도, 경도값은 openweathermap API를 이용하기 위해 필요하다.
navigator.geolocation API는 .getCurrentPosition() 메서드를 사용할 수 있다.
.getCurrentPosition() 은 success, error, option 세 개의 인자를 가질 수 있다.
각각 [ 위치정보 요청이 성공했을 때 실행할 함수, 위치정보 요청이 실패했을 때 실행할 함수, 그 외 옵션 ]을 뜻한다.
요청이 성공했을 경우에는 position이라는 값을 받을 수 있는데 여기엔 사용자의 위도, 경도를 포함한 유용한 정보들이 들어있다.
예 ) 위도 : position.coords.latitude // 경도 : position.coords.longitude
사용자의 위도, 경도값을 잘 받아왔으면 나머지는 간단하다.
fetch() 함수를 통해 데이터를 받아와 원하는 값을 뿌려주면 끝이다.
이번 프로젝트는 localStorage와 사용자의 위치값 받아오기가 가장 큰 난제였다.
그래도 완성된 작품을 이렇게 리뷰할 수 있어서 기쁘다.
이번에는 모르는 부분만 중간중간 강의를 참고했지만, 다음에는 강의를 보지 않고서 나만의 스타일로 다시 만들어봐야겠다~
'코딩 리뷰 > [HTML, CSS , JS]' 카테고리의 다른 글
[HTML, CSS, JS] 요소를 마우스 커서에 따라다니게 하기 (0) | 2023.05.18 |
---|---|
[HTML, CSS, JS] 유튜브 클론코딩 리뷰 (0) | 2023.04.26 |
[HTML, JS] 멀티 Select 공부 리뷰 (0) | 2023.04.14 |
[HTML, CSS, JS] 반응형 헤더 클론코딩 리뷰 (0) | 2023.04.10 |
[HTML, CSS, JS] 10000 시간의 법칙 클론코딩 리뷰 (0) | 2023.04.10 |