최근 갑자기 바닐라자바스크립트로 뭐라도 만들어보고 싶어졌다. 왜냐하면 바닐라자바스크립트에 대한 지식과 기술이 거의 전무하기 때문...!!!
그래서 유튜브에서 코딩앙마 님의 짧은 강의 자바스크립트 DOM & EVENT를 듣고 간단한 걸 만들어보기로 했다.
자바스크립트로 간단한 과제를 해보려고 한다는 것을 안 시니하님이 신기한 문제를 주셨고, 이번 포스팅에서는 그 문제를 푸는 과정을 포스팅하려고 한다!
구현해야 하는 화면은 다음과 같다. 아래처럼 input창에 글자를 입력하면 그 아래에서 입력한 글자가 움직인다.
사실 움직인다기보다는 글자가 규칙을 갖고 사라졌다가 다시 보이는 거지만 얼핏 보면 움직이는 것처럼 보이기 때문에 움직이는 글자
라고 이름을 짓기로 했다.
깃허브에 올린 코드 - 움직이는 글자
일단 UI를 만들어보았다. 단순하게 h1
태그에 '텍스트를 입력하세요!'를 쓰고 그 아래에 type='text'인 input 창을 만들면 될 줄 알았다.
그런데 문제가 생겼다. 화면에서는 화면의 너비가 달라짐에 따라 input창이 길어졌다가 작아졌다가 하는데, 내 화면에서는 input 창의 크기가 고정되어 있었다.
구글링을 해본 결과 width:100%
를 속성으로 주면 된다고 했다. 하지만 그렇게 했더니 화면을 최대로 크게 했을 때의 너비에 맞춰 긴 input창이 만들어지기만 했지, 화면 크기에 따라 자동으로 조절되지는 않았다.
그래서 더 찾아본 결과, box-sizing : border-box
을 찾았는데, 이 속성을 주면 의도한 대로 input창 크기가 화면의 너비에 맞춰 자동으로 늘어나고 줄어드는 것을 확인했다! (box-sizing 관련 mdn 문서)
완성된 UI는 다음과 같다.
문제 파일을 보면 input태그에 문자를 입력했을 때 일단 그 아래에 입력값과 동일한 문자열이 나타나고, 그 후에 글자가 움직인다. 따라서 일단 입력값과 동일한 글자를 화면에 나타내보자.
일단 html은 아래와 같다. input 태그 아래의 div에 input태그에 문자가 입력되면 이와 같은 문자를 출력할 것이다.
<!-- index.html -->
<input id='input' type="text">
<div id='result'></div>
input 태그에 글자가 입력되면 그 '입력을 감지하여', '그와 똑같은 문자'를 아래에 출력해줄 것이다.
그러기 위해 input 태그에 처음에는 onchange='printInput()'
라는 속성을 넣어줬었다. 그런데 동작하지 않는 것이다..!
그래서 찾아봤더니 html 'input type="text"' onchange event not working라는 글이 있었고, 그 아래에 onchange is only triggered when the control is blurred. Try onkeypress instead.라는 답변이 있었다.
blur라는 걸 배웠는데 기억이 안나서 검색해봤다! blur는 '포커스를 잃는 순간(blur)'이라고 한다. 그래서 input 창에 커서가 있으면 출력되지 않지만, 다른 곳에 커서를 클릭하면 문자가 보인다.
스택오버플로우에서 대신 onkeypress
를 써보래서 해봤다.
결과는 한 박자 늦게 입력된다. 만약에 하트라는 글자를 입력한다고 해보자. 그러면 '하'를 입력했을 때는 아무것도 출력이 안 되다가 '하트'라는 글자를 입력하면 '하'만 출력되는 방식이다.
❓ 분명히 그랬는데 해보니까 왜인지 아예 이벤트를 감지를 못한다.
👉 왜 감지를 못하는지 알아냈다!! onkeypress는 한글 입력은 감지를 못한다고 한다.
onkeyup
은 키를 누르고 뗄 때 이벤트를 감지한다. 이걸 쓰면 의도대로 동작한다! 그래서 onkeyup
을 쓰기로 했다.
addEventListener
를 써보자addEventListener
를 쓰는 것이 더 좋은 방법이라고 한다.
이벤트를 발생시킨다는 점은 둘 다 동일하다. 하지만 addEventListener
는 하나 이상의 핸들러를 등록할 수 있는 것에 반해 전자(onChange
, onClick
, onKeyPress
, onKeyUp
등)는 이후에 추가된 이벤트가 기존의 이벤트를 덮어써 하나의 핸들러만 등록할 수 있다.
<이벤트와 이벤트 핸들러>
- 이벤트(Event): 웹사이트를 방문한 사용자가 수행하는 동작
- 이벤트 핸들러(Event Handler): 이러한 동작이 발생할 경우 처리할 실제 내용
이걸 대체 어떻게 해야 할까..? 일단 입력이 더 이상 없으면 글자가 앞에서부터 2개씩 2개 간격으로 사라졌다가 보이고 있다. 이걸 어떻게 구현해야 할까...
이런 시도를 해보기는 했다... 당연히 안됐다.
function printInput() {
function ordinaryPrint() {
let inputValue = document.getElementById('input').value;
document.getElementById("result").textContent = inputValue;
};
ordinaryPrint();
function disappearPrint() {
let inputValue = document.getElementById('input').value;
for (let i=0; i<inputValue.length; i++) {
if (i%4 === 0) {
inputValue = inputValue.replace(inputValue[i], ' ').replace(inputValue[i+1], ' ');
document.getElementById("result").textContent = inputValue;
}
}
};
disappearPrint();
}
글자가 사라진 것처럼 보이게 하려고 replace()함수를 사용하여 어떤 인덱스에 있는 글자를 스페이스로 바꿨다.
❌ 하지만 이렇게 하면 큰 글자(예를 들면 한글)도 스페이스로 바뀐다. 123과 가나다는 너비가 달라 너비에 맞게 빈 공간이 생겨야 하지만 모든 글자를 스페이스로 대체하면 모두 너비가 같은 빈 공간이 생기게 된다.
for문에서 if (i%2 === 0)
를 쓰면 짝수와 홀수번째에 있는 글자를 쉽게 스페이스로 대체할 수 있다. 하지만 두 글자씩 사라지게 하는 건 어떻게 해야 할지 모르겠다.. 아 헐 혹시 inputValue.length
로 문자열의 개수를 세어서 그걸 4개씩 나눈게 문젠가?? 그래서 문자열의 길이가 바뀔 때마다 띄어쓰기되는 문자가 이상하게 선택된 걸까???
두 글자씩 선택하는 법을 어떤 분이 알려주셔서 알았다.
j가 0부터 시작하는 for 문에서 인덱스가 [4*j+2]
와 [4*j+3]
가 되면 되므로 이에 해당하는 글자를 replace()
를 이용해 바꾸면 된다.
그리고 어떻게 구현하면 좋을지 주석을 달아봤다.. 구현하지는 못했다. 충분하게 고민해본 것 같아 이 문제를 주신 분께 힌트를 얻으려고 한다!!!
function printInput() {
// 입력받은 값을 아래의 div에 똑같이 써준다.
function ordinaryPrint() {
let inputValue = document.getElementById('input').value;
document.getElementById("result").textContent = inputValue;
};
ordinaryPrint();
function disappearPrint() {
// 무한 반복한다. 새로운 값이 입력되기 전까지
// while (새로운 값 입력되기 전)) {
let inputValue = document.getElementById('input').value;
// 이중 for문 시작,,, 오른쪽으로 움직이도록 인덱스를 증가시킨다?
// 근데 끝을 지나면 앞으로 다시 돌아오게 해야 함
// for(let i=0; i<inputValue.length; i++) {
// 2글자 간격으로 2개씩 글자를 스페이스로 대체한다
for (let j=0; j<inputValue.length; j++) {
inputValue = inputValue.replace(inputValue[4*j+2], ' ').replace(inputValue[4*j+3], ' ');
console.log(inputValue)
document.getElementById("result").textContent = inputValue;
}
// }
// }
}
disappearPrint();
}
setTimeout
와setInterval
를 써야한다!
둘 다 잘 몰라서 찾아봤다.
setTimeout(functionRef, delay, param1, param2, /* … ,*/ paramN)
setTimeout()
는 첫 번째 인자로 실행할 코드를 담고 있는 함수를 받고, 두 번째 인자로 지연 시간을 밀리초(ms) 단위로 받는다.
세 번째 인자부터는 가변 인자를 받는다. 첫 번째 인자로 넘어온 함수가 인자를 받는 경우, 이 함수에 넘길 인자를 명시해 주기 위해서 사용한다.
👉 가변 인자는 함수의 매개변수 개수가 고정적이지 않고, 변할 수 있다는 뜻
(예시)
아래는 5초를 기다린 후에 addFunction 함수의 x, y 인자로 각각 1, 2를 넣어 호출하는 예시이다.
function addFunction(x, y) {
console.log(x + y);
}
setTimeout(addFunction, 5000, 1, 2);
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
어떠한 코드를 일정한 시간 간격을 두고 반복해서 사용하고 싶을 때 사용한다.
(예시)
현재 시간이 2초마다 출력되는 예시이다.
setInterval(() => console.log(new Date()), 2000);
이 함수는 인터벌 아이디(Interval ID
)라고 불리는 숫자를 반한다.
인터벌 아이디는 setInterval()
를 호출할 때마다 내부적으로 생성되는 타이머 객체 인터벌 아이디를 가지고 있으며, 이 값을 인자로 clearInterval()
를 호출하면 코드를 중단시킬 수 있다.
(예시)
이제 힌트를 얻었으니.. setTimeout
와 setInterval
를 사용해 보자..!!
글자가 입력되면 두 글자 간격으로 두 글자를 사라진다.
사라지게 하고 한 칸씩 옆으로 이동하여 글자를 사라지게 한다.
무한으로 반복한다.
일단 지금 생각나는 것은 좀 더 쉬운 버전으로 시도해 보자는 것이다.
따라서 멈춰있는 문자열으로 테스트를 먼저 해보자!
👉 그렇게 해보려고 했으나... 일반 자바스크립트
vs. 브라우저의 HTML을 조작하는 자바스크립트
는 너무 다른 것이었다..(이 표현이 맞는지 모르겠다) 하여튼 그래서 더 쉬운 코드로 연습해 보려다가 그만두고 원래 코드로 돌아왔다.
아래가 내 최선이었다,,,,, 근데 이제 실행하면 input태그 아래에 아무것도 안 나오는,,,,
// main.js
function printInput() {
let letters = document.getElementById('input').value;
let timerId = setInterval(() => {
// 한칸씩 인덱스 옆으로 움직이게끔
// for문으로 구현 가능한가..?
for (let i=0; i<timerId.length; i++) {
letters = letters.replace(letters[4*i+2], ' ').replace(letters[4*i+3], ' ');
document.getElementById("result").textContent = letters;
console.log(letters);
// 새로운 글자 입력되면 멈추고 처음부터 하도록
// if () {
// clearInterval(timerId)
// }
}
}, 3000);
}
👉 for문에서 timerId.length가 아니라 letters.length였다.
이렇게 하니까 input 태그 아래에 정상적으로 무언가 출력이 되기는 했다!
그리고 두 번째 힌트를 얻었다.
1. 동작 방식은 각 글자가 500ms 간격으로 투명해졌다 나타났다 하는 것
2. 글자 하나마다 250ms의 딜레이가 들어가서 마치 글자가 움직이는 것처럼 보이게 된다
그러니까 나는 글자를 실제로 for문을 이용해서 인덱스를 늘려가며 글자를 움직이려고 했는데 사실은 글자 하나마다 딜레이가 들어가서 마치 글자가 움직이는 것처럼 보이게 하는 것!
분명히 중요한 힌트를 들었는데... 모르겠는 기분은 뭘까 (실제로 이해 못 한 게 맞음)
이해를 잘 못한 채로 다시 시도해 보기로 했다. 일단 글자가 500ms 간격으로 투명해졌다 나타났다 하는데, 글자마다 250ms의 딜레이가 있다는게..무슨 말인지 해석이 필요한 것 같다.
일단 모든 문자들을 투명해졌다가 나타나게 해보자.
2개 간격으로 투명해졌다가 나타나게 해보자. (이 부분이 내가 문제를 잘못 해석한 부분...)
글자마다 딜레이를 넣어보자
setInterval
과 setTimeout
을 써봤다. 이게 나의 최선이었는데,,, 뭔가 지금까지 한 것 중에 그나마 좀 정상적인 결과물이 나왔다(답이랑은 상관없는 것 같지만)
// main.js - 일단 한 글자(빈칸)씩 오른쪽으로 밀려가는 것 같은 걸 해보려고 햇음..
function printInput() {
// 입력받은 대로 똑같이 써주기
let letters = document.getElementById('input').value // 12345
document.getElementById("result").textContent = letters; // 12345
// <문제>
// 12345가 입력되면 12345를 입력으로 받아야 하는데 1, 12, 123, 1234, 12345를 입력으로 받음.
// --> 그래서 지금 ' 2345' '1 345' '12 45' '123 5' '1234 ' 이렇게 되어야 하는데
// --> ' ', '1', '12', '123'이 나오고 있음...
// 글자가 하나씩 사라졌다가 나타나는 것을 500ms에 한 번씩 계속 반복한다는 뜻으로 짰음,,,
// 근데 하나씩 글자가 나오기만 하잖아..? 다 있는 상태에서 하나씩 사라져야 되는데..
let timerId = setInterval(() => {
for (let i=0; i<letters.length; i++) {
after = letters.replace(letters[i], ' ');
setTimeout(() => document.getElementById("result").textContent = after, 500);
}
}, 1000)}
여기서 도저히 어떻게 할지 모르겠어서 힌트를 또 받았다 하핫
1. 각 글자를 별도의 element(span 등)로 두면 훨씬 편하다
2. 각 글자를 스페이스로 대체하는 게 아니라.style.opacity="0"
라는 걸 사용하면 투명으로 바꿀 수 있다.
3. 근데 .style.opacity는 DOM 객체 하나를 담당하므로 앞에 DOM이 아니라 string같은 거는 써도 소용이 없다.
특징: 세 힌트를 순서대로 모두 다른 분께 받음. 정말 웃기지만 안웃기다🤣
글자 색을 투명하게 바꾸려고 after = result[i].style.opacity = "0";
라고 적어놓고 오류가 떠서 한참 삽질하다가 .style을 쓰려면 앞에 style을 적용할 DOM element
를 써야 한다는 것을 알게 됐다. result는 getElementById
로 DOM element를 가져왔지만 뒤에 innerHTML을 붙여 더이상 DOM element가 아닌 문자열이 되었다. (이렇게 정의함: result = document.getElementById("result").innerHTML
) 그래서 오류가 뜬 것이다.
DOM element는 createElement
등을 써서 만들 수 있다.
style을 쓰려면 이와 같은 DOM element 뒤에 써야 한다!
힌트를 듣고... 일단 아래와 같이 짰다. 그런데 id를 콘솔로 찍어보면 자꾸 null이 뜨는 것이다..
// let timerId = setInterval(() => {
for (let i=0; i<letters.length; i++) {
// 빈 span 태그 만들기
let whiteLetter = document.createElement('span');
// span에 id를 인덱스로 부여하기
whiteLetter.id = `${i}`;
// span에 글자 넣기.
whiteLetter.textContent = result[i];
// console.log(whiteLetter);
// DOM 객체 하나에 들어감. result는 string라서 안됨..
// console.log(`${i}`);
id = document.getElementById(`${i}`);
console.log(id);
setTimeout(() =>
id.style.color = "blue",
1000
);
}
// }, 1000)
다시 질문을 드렸고, 어디가 틀렸는지 알려주셨다.
일단 id를 만들 필요가 없었다. whiteLetter의 결과가 id의 결과와 같기 때문에!
그리고 색이 변한 글자가 화면에 보이지 않는 이유는 색을 바꾼 것은 잘 했는데 그 이후에 appendChild
와 같은 함수로 페이지 어딘가에 추가하지 않았기 때문이다.
결론은 아래와 같다.
- id 부분 삭제하고
whiteLetter
쓰기appendChild
로 페이지에 바뀐 글자 추가하기
// let timerId = setInterval(() => {
for (let i=0; i<letters.length; i++) {
// 빈 span 태그 만들기
let whiteLetter = document.createElement('span');
// span에 id를 인덱스로 부여하기
whiteLetter.id = `${i}`;
// span에 글자 넣기.
whiteLetter.textContent = result[i];
// console.log(whiteLetter);
// DOM 객체 하나에 들어감. result는 string라서 안됨..
// console.log(`${i}`);
whiteLetter.style.color = "blue";
document.getElementById("result").appendChild(whiteLetter);
// setTimeout(() =>
// 1000
// );
// }
// }, 1000)
}
}
그럼 아래와 같이 입력하는 대로 뒤에서 파란색 글자가 출력됨을 알 수 있다. 이제 어떻게 하지...
중간에 글자가 출력되게 하려고 아래와 같은 방법을 써봤는데, 모두 오류가 났다...^_^
document.getElementById(`${i}`).appendChild(whiteLetter); // 오류
document.getElementsByTagName('span').appendChild(whiteLetter); // 오류
이젠 정말 모르겠다!!!! 일주일이면 충분히 고민해 본 것 같다... 이젠 정답을 듣고 공부하는 것이 좋을 것 같다고 생각했고 시니하님께 문제 해답에 대한 설명을 들었다.
아래는 정답 코드와 내가 작성한 주석이다!
(처음부터 친 게 아니라 내 코드를 고친 거라 시니하님의 코드와는 차이가 있다)
결론은 아래와 같다.
1.
setTimeout
으로 입력한 각 글자를 새로운 element로 가져와서 250ms마다 색이 '투명->검정', '검정->투명'으로 바뀌게 하기(여기까지 하면 전체 글자가 깜빡거리는 것이 보인다)
2.setInterval
로 250ms마다 글자에 딜레이를 주기(이렇게 하면 글자가 움직이는 것처럼 보인다)
3. 최적화하기(적절한 시기에clearInterval
,clearTimeout
가 호출되게 하여 setTimeout과 setInterval이 불필요하게 호출되지 않도록 한다.)
// input의 Dom element를 가져오기
const input = document.getElementById('input');
// addEventListener로 이벤트 등록, printInput 이벤트 핸들러 실행되게 하기
input.addEventListener('input', printInput);
// result라는 ID를 가진 DOM을 resultDiv라는 이름으로 가져오기
const resultDiv = document.getElementById('result');
// 각 id들이 생성될 때마다 담을 리스트 생성
let intervalIds = [];
let timerIds = [];
function printInput() {
// input 태그에 입력한 값 letters로 가져오기
const letters = input.value;
// setInterval을 벗어나면 실행되고 있는 모든 interval을 중단시킴
intervalIds.forEach(clearInterval);
// intervalIds 초기화
intervalIds = []
// result 내용 초기화
resultDiv.innerHTML = "";
// setTimeout 중단시키기
for (let i = 0; i < timerIds.length; i++) {
clearTimeout(timerIds[i]);
}
// timerIds 초기화
timerIds = []
// letters의 길이만큼 for문 돌리기
for (let i = 0; i < letters.length; i++) {
// span element 생성하고 이름은 whiteLetter라고 하기
const whiteLetter = document.createElement("span");
// span에 글자 넣기
whiteLetter.textContent = letters[i];
// 각 글자 i가 250ms 간격으로 딜레이되도록. 위의 for문을 통해 마지막 글자까지 반복
const timeoutId = setTimeout(() => {
// 500ms 마다 글자 색을 바꿔줌(투명 --> 검정 --> 투명 --> 검정 ...(반복))
const intervalId = setInterval(() => {
// 글자 색을 투명으로 바꾸는 삼항 연산자
whiteLetter.style.opacity = whiteLetter.style.opacity === "0" ? 1 : 0;
}, 500)
// intervalIds 리스트에 매번 생성되는 intervalId 추가
intervalIds.push(intervalId);
// 새로운 interval이 추가될 때마다 console 찍어보기
console.log('Interval 새로 추가함, 현재 intervalIds:', intervalIds)
}, 250)
// timeout Id가 생성될 때마다 timerIds 리스트에 추가하기
timerIds.push(timeoutId);
// result element 부분에 whiteLetter를 더하여 화면에 나타냄
result.appendChild(whiteLetter);
}
}
일단 어느 부분에서 삽질했는지 알아보자.
첫 번째로는 입력한 글자가 두 글자씩 없어지고 나타나는 줄 알고 엄청 삽질했던 것 같다. 완성된 화면을 봤을 때 한 글자씩 움직이는 게 아니라 두 글자씩 움직이는 것으로 보였기 때문! 그래서 두 글자를 투명으로 만들고 그 다음 두 글자는 검은색으로 하고 그 다음 두 글자는 투명으로 만들고... 하려고 삽질을 했었다.
그런데 250ms와 500ms를 각각 10배씩 느리게 해서 봐도 두 글자씩 없어졌다가 나타나는 것을 발견했다. 왜 그러는 걸까? 해서 여쭤봤다.
정리해 보면,
setInterval로 500ms동안 보이고 안보이고를 반복한다.
setTimeout으로 250ms동안 한 글자씩 집어 250ms의 딜레이를 주며 마지막 글자까지 반복한다
1이 250ms 동안 안 보이고 다시 2가 250ms 동안 보임->안보임이 되는 동안 1은 남은 250ms 동안 안 보이는 상태로 남아있고 500ms가 끝나면 1은 다시 보이고 2는 남은 250ms 동안 여전히 안 보이고 3이 이제 250ms동안 보이고.. 가 반복된다.
이해했다!!!!
이전에 아래와 같은 힌트를 얻었는데, 전혀 이해하지 못했었다. 왜 이해하지 못했는지를 추적해 보니, '딜레이'가 무슨 말인지 이해를 못 했던 것 같다.
1. 동작 방식은 각 글자가 500ms 간격으로 투명해졌다 나타났다 하는 것
2. 글자 하나마다 250ms의 딜레이가 들어가서 마치 글자가 움직이는 것처럼 보이게 된다
그러니까, setTimeout
으로 250ms 간격마다 마지막 글자까지 한 글자씩 선택하는 것이 딜레이인 줄 몰랐다.
이렇게 하나씩 선택하여 setInterval을 하면 그 글자가 500ms씩 보였다 안 보였다 하는데, 이게 착시를 일으켜 움직이는 것처럼 보이게 된다.
이러한 이유로 삽질을 했다.. 일주일 동안.. 중간중간 많은 힌트를 받았지만 결국 풀지 못했다. 그런데 답을 보니 못 풀고 허우적댔던 내가 이해간다..! 내 수준에 어렵기도 했지만 setTimeout
과 setInterval
을 안 써본 것도 못 푼 큰 이유가 된 것 같다.
사실 남은 과제가 있다. 바로 이것이다! 현재까지는 설명 듣고 수정하기, 완성한 코드 보고 똑같이 코드 따라서 쳐보기까지 했다.(주석과 함께. 나중에 코드는 지우고 주석을 보면서 복기해볼 것임)
혼자서 안 보고 다시 코드를 짜며 하는 공부가 아주 중요하다고 생각하기 때문에 이렇게 정해봤다. 최근에 올린 투두리스트 CRUD 등도 이와 같은 공부법을 적용한 것이다.
이후에는 주석만 보고 코드를 짜고, 다 지우고 혼자 힘으로 구현해 볼 것이다. 오늘은 시간이 부족해서 다 못 끝냈지만, 꼭 한 번은 해보고 넘어가려고 한다! 앞으로 화이팅!!
신기한 문제를 알려주시고, 또 중간중간 힌트를 주시고 오류도 찾아주시고 마지막에는 해답까지 친절하게 설명해 주신 시니하님 감사합니다!! 🙇🙇