요즘은 핸드폰으로 본인인증 많이 하는데, 그 인증창 보면 딸랑 이름 입력하는 란만 보인다. 그리고 이름을 입력하면 주민번호 입력란이, 주민번호 입력란을 다 채우면, 핸드폰 번호 입력란, 그리고 보안문자 입력란(이건 대충 사람입니다 체크하는거라고 보시면 될듯) 다음에 인증번호 입력란이 순차적으로 뜬다. 그러니까 이게 처음부터 떠있는 게 아니라 어떤 조건을 만족하면 뜬다 이거다.
근데 여기서 중요한 게 있다. 우리는 저기에 뭘 '채워 넣었을 때' 다음 창이 보이게 해야 하잖아요? 그러니까 이벤트 리스트너를 쓸 거면 쟤를 클릭할때 되게 하면 안 된다. 그리고 위에 뭐가 길어서 봤더니 입력란 달린 애들은 다 두줄씩 달고 있죠? 보여야 하는 건 div인데 값 갖고와야 하는 건 입력란이라 그렇다. 첫번째줄은 div, 두번째줄은 입력란이라고 생각하면 된다.
근데 애초에 저기에 이벤트 리스트너를 줄 수가 없음… 내가 해봤는데 안됨. 그럼 어떻게 해요? 값 가져와서 if문 때려박아봐야지.
이벤트 리스트너의 blur를 키다운으로 바꾸고 if문에 1) 이름이 입력된 상태에서 2) 엔터키를 눌렀을 때 다음 입력란을 볼 수 있게 하면 된다. 근데 쟤는 위에 썼던 코드랑 달리 트리거 옆에 e가 있네요? 님 원래 저거 생략하지 않았음? 저걸 생략한 게 익명함수 김람다씨인데 김람다씨가 아무때나 다 쓸 수 있는 게 아니다. 마치 우리가 인터넷에서는 닉네임으로 활동하지만 송금할때는 본명이 필요하듯이 말이다. 김람다인 이유는 그냥 김씨가 흔해서다 그럼 저 e가 없으면 어떻게 되는데요? 컴퓨터가 타겟을 못 찾아서 뭔 엔터를 뭘 어쩌라는거냐면서 밥상을 뒤집습니다. 물론 진짜 밥상을 뒤집지는 않겠지만 아마 의도한대로 작동은 안 할거다.
깜빡하고 CSS 생략할 뻔 했는데, 보통 프로그레스 바는 화면 상단에 있다. 그러니까 위치를 화면 상단으로 고정해야 한다.
이 스크롤바 역시 이 블로그에 단골로 등장하는 이벤트 리스트너를 이용할건데... 그럼 여기서 짬 좀 있으신 분들은 감이 좀 오실 것이다. 아니 이거 스크롤 이벤트도 커버가 돼요? 아니 글쎄 그것까지 커버가 되더라고요? 얘 대체 안되는게 뭐임?
document.addEventListener('scroll',()=>{
let scrollNum = window.scrollY;
console.log(scrollNum);
})
이런 식으로 스크롤 높이를 가져온 다음 거기에 맞춰서 작대기 크기를 늘였다 줄였다 하면 된다고 보시면 된다. window.scrollY 말고 document.documentElement.scrollTop도 쓴다.
document.addEventListener('scroll',()=>{
let scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
let progressPercent = window.scrollY / scrollHeight
console.log(progressPercent)
})
뭐임? 왜 갑자기 많아짐? 을 이제 하나씩 풀어보자...
document.documentElement.scrollHeight은 본문에 로렘 입숨을 급나 때려박아서 만든 높이이다. 그러니까 캡쳐된 부분 말고 그 밑에 더 있는 본문까지 해서 나오는 높이를 말한다. 그리고 document.documentElement.clientHeight은 나도 뭔지 모르겠음… 패딩을 포함한 요소의 높이라는데 이건 좀 더 알아봐야겠음.
그래서 이게 뭐 하는건데요? 저 작대기 길이 정할라면 계산을 해야 할 거 아닙니까. 프로그레스 바 길이 정할라고 계산하는거다.
무슨 차이인지 모르겠다고? 안 되는 코드는 이벤트 리스트너에 click이 들어가 있고, 되는 코드는 이벤트 리스트너에 change가 들어가 있다. 아니 지피티야 이거 왜이럼?
클릭은 쟤를 클릭하면 이벤트가 시작된다. 그러니까 파일을 첨부하기 '전에' 이벤트가 발동되는데, 컴퓨터 입장에서는 뭐여 왜 없는 파일 제목을 띄우래? 가 되는거다. 마치 내 남친은 질량이 0인데 남자친구를 데려오라는 것과 같다. 그리고 체인지는 파일이 올라왔을 때 발동되는거라 아 올라왔으니 띄워줌이 되는 것이다. 이해가 어렵다면 change는 '(파일의 업로드) 상태가 바꼈을 때'라고 이해하면 된다.
불러온 후
참고로 원래 기능(텍스트 내용 복사, 텍스트 저장)역시 파일을 불러온 후로도 제대로 작동한다.
프리텐다드.. 솔직히 고딕체는 딱딱해서 별로 안 좋아하지만 가장 깔끔한 것도 고딕체이긴 하다. 깔끔한건 인정. 근데 내가 고딕체를 별로 안 좋아하는 이유도 특유의 딱딱함때문임...
리디바탕. 바탕체중에서 개인적으로 제일 좋아하는 바탕체이다.
도스이야기. 왜 초딩때 했던 한컴타자가 생각나는거냐...
마비옛체
참고로 이 글꼴들을 고를 때 본인 기준으로 한자 지원 안 하는 폰트는 다 빠진다. 이게 단순히 지원만 안 하는거면 구글 웹폰트에서 일본 폰트 긁어와서 쓰면 되는데 한자 지원을 안하면서 한자를 공백으로 표기하면 골치아파짐… 특히나 괴담수사대에는 제목이나 본문에 한자가 들어가는 경우도 있어서 그런 폰트는 기피된다.
일단 동적으로 생성할 div의 수를 입력받아보자. HTML? CSS? 그게 지금 중요한 게 아녀… 이 코드의 코어이자 빡셈을 담당하는 자바스크립트를 봐야 한다 이거요.
사진이 한 장일때
일단 사진이 한 장일때를 빼면 무조건 3n+2n으로 들어가는 구조인데, 여기에 맞춰서 div를 우리가 동적으로 생성해야 한다. 이거 어디서 비슷한 문제 봤다고요? 예에에에에에에에전에 그리디 알고리즘 풀 때 봤던 것 같죠? 거스름돈 640원을 가장 적은 수의 동전으로 지불하려면 어떻게 해야 할 지. 뭐 그리디 알고리즘이 전체적으로 최적일거라는 보장은 없지만, 사진 올린거 배치하는데 전체적으로 최적이고 자시고가 어디 있음.
if (photoRemain == 1) {
const photoRow = document.createElement('div');
photoRow.className = 'grid-row';
photoContainer.appendChild(photoRow);
}; //사진이 한 장만 있을 때
일단 사진이 한 장일때를 따로 처리해야 한다. 사진이 하나밖에 없으면 3n+2n으로 못 하거든… 3n+2n에 0을 때려박지 않는 이상 1이 더 작아요. 아무튼 그래서 예외처리 한다 보시면 됩니다.
자바스크립트에도 while문이 있냐고? 있다. 잘 안 쓸 뿐이지. 아무튼 사진이 2장 이상이면 3x+2y꼴로 나타내라는 얘기인데... 이게 문제가 있어요.
일단 이게 첫번째 문제다. 최대 3장씩 가로로 나열돼야 맞는데 저기 보시면 세로로 줄줄이 있죠? 이거는 JS가 아니라 CSS를 건드려서 해결 봐야 하는 문제라서 디스플레이를 flex로 주면 된다. 근데 저게 첫 번째 문제면, 다른 문제가 또 있다는건가요? 그렇다.
사진은 13장인데 한장이 짤렸다. 내가 지피티한테 3n+2n으로 나와야 한다고 했는데… 얘 더워먹었나…
if (photoRemain == 1) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
photoContainer.appendChild(photoInbox);
return;
}; //사진이 한 장만 있을 때
지피티는 가끔 말을 해줘도 씹어먹을 때가 있어서 매번 리마인드를 해 줘야 한다는 단점이 있다. 얘 그림 그릴때도 A 수정하면 B 망치고 B 수정하면 다시 A 망치고 반복함… 실화예요. 이것때문에 이미지 리사이징 포기하고 구글 whisk에게 그려달라고 했다니까.
그래서 뭘 어떻게 바꾼건데요? 일단 사진 한장짜리는 크게 바뀐 거 없다. 인박스 안에 사진이 들어가는 구조긴 한데 그게 다거든. 그래서 크게 바꿀 건 2장 이상 말고는 없다.
while (photoRemain > 0) {
let photosInrow;
if (photoRemain % 3 === 1 && photoRemain >= 4) {
photosInRow = 2; // 1장 남을 미래 방지
} else if (photoRemain >= 3) {
photosInRow = 3;
} else if (photoRemain == 2) {
photosInRow = 2;
} else {
console.log('어? 뭔가 이상한데?');
break;
}
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let i = 0; i < photosInRow; i++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
photoRemain -= photosInRow;
}
얘는 대체 뭔 미래를 방지한다는건지 모르겠음... 시키는거나 똑띠 했으면 좋겠구만.
일단 while문의 if문 맨 위를 보자. 저 문제가 터진게 4, 7, 10인데 1을 제외하면 3n+1이라는 공통점이 있다. 근데 내가 지피티한테 예시로 설명까지 들어가면서 설명했던 배치는 '2장 이상이면 무조건 3x+2y 형태로 배치가 되어야 한다'였고, 저대로라면 4장은 2+2, 7장은 3+2+2, 10장은 3+3+2+2가 되어야 하는데 내 요청을 또 무시하고 3n+1로 만들어버린 것. if문 맨 위는 사진 장 수를 3으로 나누기했을 때 1이 남았거나(3n+1) 4장이면 사진을 한 줄에 두 장 넣으라는 얘기이다. 7장에서 두장 빼면 5장은 3n+2니까 기존 로직으로 해결이 된다 이거지.
그럼 이걸로 끝인가요? 아뇨.
내가 표현식을 3x+2y로 쓴 건 3장짜리가 먼저 올라오기 때문이다. 그니까 사진이 7장이면 3+2+2, 10장이면 3+3+2+2, 13장이면 3+3+3+2+2 이런 식으로. 근데 지금 보시면 7장이 2+3+2로 올라오잖음? 근데 이것까지는 내가 지시를 안 했으니 그럴 수 있다 치자.
let photoRemain = countNum;
let photoIndex = 1;
if (photoRemain == 1) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
photoContainer.appendChild(photoInbox);
return;
}; //사진이 한 장만 있을 때
if (photoRemain === 4) {
for (let i = 0; i < 2; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
return;
}//4장은 2+2
// 그 외 케이스
let threeRows = Math.floor(photoRemain / 3);
let extra = photoRemain % 3;
// 3장씩 먼저 배치
for (let i = 0; i < threeRows; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 3; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
// 3n + 1 → 2 + 2 배치
if (extra === 1) {
for (let i = 0; i < 2; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
}
// 3n + 2 → 2장 배치
else if (extra === 2) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
저게 다 뜨는 게 아니고 3n+1… 1, 7, 10에서 뜬 걸 확인했다. 4는 원래 4장이라 패스. 내가 저거 실행해보고 하도 얼척없어서 지피티한테 이게 맞냐고 물었음.
사실 코드가 길고 복잡해보이지만 전체적으로 보면 사진 장수를 3장씩 먼저 올리고 남은걸 두 장씩 올리면 되는 매우 간단한 코드이다. 근데 뭐가 문제임? 이게 3n+2(5, 8, 11)장일때는 3장을 n줄씩 올리고 밑에 두 장이 들어가면 되는데, 3n+1이 되니까 나머지를 계산하는 과정에서 문제가 생긴 것이다.
let threeRows = Math.floor(photoRemain / 3);
let extra = photoRemain % 3;
이게 7장일때, 8장일때는 똑같이 threeRows가 2지만 extra가 각각 1, 2다. 그러니까 6+1, 6+2인건데
저게 왜 문제냐면 ===로 하면 타입까지 비교하게 되는데, 막 input을 통해 입력받은 숫자는 숫자가 아니라 string… 그니까 문자다. ===를 쓰게 되면 컴퓨터 입장에서는 얘는 문자고 쟤는 숫자니까 다른거지만, ==를 쓰면 어쨌든 1이니까 아 ㅇㅋㅇㅋ 하게 되는 것. 이 사태를 예방하려면 아예 let countNum = photoCount.value;에서 등호 오른쪽을 parseInt로 감싸는 방법도 있다.
이번에 애플이 iOS 26을 발표하면서 리퀴드 글래스인가 뭐시기인가를 도입한다고 했는데… 우리 쿡이는 19 20 21 22 23 24 25를 못 세나요? 왜 갑자기 26으로 건너뛰었음? 우리 쿡쪽이는 수학공부를 안 했나?
각설하고 우리도 그 리퀴드 글래스인지 뭔지 하는 그 효과 좀 내 보자. 근데 애플처럼 깔쌈하게 나올거라는 장담은 못 드림. 잊지 마십시오. 이 블로그 주인장은 뉴비입니다. 애플처럼 진짜 깔쌈한 유리 효과를 내고 싶으시면 여기 말고 다른 블로그를 가십시오.
일단 유리 하면 투명하다. 아무것도 안 묻은 새삥 유리는 투명한데… 문제가 하나 있다. 이걸 그대로 CSS에 도입하게 되면, 배경에 뭐가 있을 경우 콘텐츠가 안 보일 위험이 있다. 정확히는 사용자가 콘텐츠에 집중을 못 할 위험이 크고, 내용을 알아먹는것도 빡세다. 그래서 보통은 뒤에 블러를 준다. 왜 예전에 모달창 설명 할때도 모달창을 열 동안 배경에 블러를 줬죠? 그거 비슷합니다.
const musicUpload = document.querySelector('#uploader');
var audio = new Audio(musicUpload.file);
audio.load();
audio.volume = 1;
audio.play();
원래 이런건 한번에 되는 게 이상한거긴 한데... 왜 안되는지 모르겠음. audio.play()에서 Uncaught (in promise) NotAllowedError: play() failed because the user didn't interact with the document first. 라는 오류가 나고 있다.
그리고 이렇게만 하면 된다. 이렇게 하고 음악 파일을 열고 버튼을 누르면 audio태그가 뿅 하고 나온다. 이제 버튼 위치 잡아야지… ㅡㅡ
아, 참고로 오디오 태그가 추가된 버전은 자동재생이 안되기때문에 재생버튼을 눌러야된다.
오디오태그는 커마가 안되나요?
결론부터 말하자면 길이 외에는 커마가 안 된다. 저게 길이 늘린 버전이고, 직접 본인이 만들거나 라이브러리를 받아와야 한다…
지피티의 힘으로 커마 완료함. 반복재생이요?
그것도 커마 했다.
여기까지 지피티의 힘을 빌려서 커마 끝… OTL 이게 하나 하면 또 추가하고싶고 이거 추가하면 또 추가하고싶고 해서 해달라는게 점점 늘어요… 얘는 과정은 따로 서술 안 하고 깃헙에 올리기만 하겠습니다. 참고로 글꼴은 변경했고 생각보다 한글 글꼴중에 한자 지원 안 하는 글꼴이 많아서 구글 웹폰트에서 일본어 폰트 따로 추가했습니다.
토스트 창이 뭐냐… 가끔 뭐 하다보면 뿅 하고 나오는 그거 맞다. 예를 들자면 메일을 보냈을 때, 화면 한 켠에서 보냈습니다! 하고 뿅 나왔다가 사라지는 그거 말이다.
일단 들어가기 전에 생각해보자. 토스트창을 왜 쓰나요? 모달창도 있고 alert()로 알림을 띄울 수도 있는데 굳이 토스트 창을 만드는 이유가 뭘까? 모달창과 alert()로 띄우는 알림은 사용자가 상호작용을 해야 없어진다. 뭐 예를 들어서 오류가 났다, 그거는 내가 확인하고 상호작용 해야 하는거니까 모달이나 alert()를 쓰는 게 맞다. 오류가 났으면 오류메시지를 통해서 뭔 오류가 어떻게 터졌는지 보고 확인하고 문의를 하든 대처를 해야 하니까. 근데 그게 상대적으로 사소한거라 굳이 모달이나 alert()로 안 해도 되는 알림이라면?
예를 들어서 쿠팡에서 냉동 볶음밥 1+1 패키지를 장바구니에 담았다고 해 보자. 그럼 장바구니에 담겼다고 알림이 나한테 가면서 장바구니 아이콘에 있는 숫자가 +1 될것이다. 이건 어떻게 보면 상대적으로 사소한 알림이기도 하고, 사용자 입장에서는 장바구니 아이콘 숫자로 확인할수도 있는데 이걸 굳이 모달이나 alert()로 띄워야 할 필요가 있을까? 물론 모달창으로 만들고 장바구니 보기 버튼을 눌러서 장바구니로 가갈지말지 선택하게 할 수도 있다.
반대로 생각해보자. 냉동 볶음밥 1+1 패키지를 담고 결제를 하는 과정에서 뭔가 문제가 생겼다면 내가 문제가 생겼다는 걸 인지하고 다시 결제를 하던가 잔액부족이면 잔액을 채워넣던가 해야 한다. 그럴 때는 토스트 창으로 에러 메시지를 뿅 나왔다가 뿅 사라지게 하면 내 입장에서는 이게 구매가 된건지 어쩐건지 모를 것이다. 이렇게 꼭 한 가지만 고집할 필요도 없고, 꼭 한가지를 배제할 필요 없이 적재적소에 잘 쓰면 된다. 그래서 왜 비교적 사소한 알림에 토스트 창을 쓰냐고? 그런것까지 일일이 버튼 누르기 귀찮자낭.
버튼은 라디오버튼이나 체크박스처럼 if문으로 못 주나… 아무튼, 이렇게 하면 버튼을 눌렀을 때 토스트 창이 나타난다. 근데 문제가 하나 있다. 토스트창은 나타난 다음에 일정 시간이 지나면 사라져야 하는데, 이 창이 사라지질 않아요. 왜? JS로 투명도 1로 만드는 것만 했잖음. 일정 시간이 지나면 다시 투명도가 0이 되게 해야 토스트창이 사라지지.
기존의 아이디 생성기는 생성된 아이디를 직접 드래그해서 컨트롤씨를 눌러야 복사가 됐다. 솔직히 붙여넣는건 그렇다치고 복사까지 그렇게 한다고요? 아 귀차낭. 그래서 복사버튼을 추가해봤음. 그래서 전체적으로 어떻게 바꼈느냐…
1. 전체적인 디자인 변경 2. 체크박스에 라벨 추가(이거 해두면 글자 눌러도 체크박스 선택됨) 3. 입력창 포커스 상태 CSS 추가 4. 생성되는 아이디에 복사버튼 추가(복사시 우상단에 토스트 메시지 생성됨)
이렇게 됐다.
일단 이전 글과 달리 이번에 추가할 버튼... 요소가 HTML에 없다. 왜냐하면 아이디 생성기는 아이디 만들 때 p태그를 동적으로 생성하고, 이번에 할 일은 p태그를 동적으로 생성할 때 버튼을 '같이' 생성해서 오른쪽에 나란히 붙여줘야 한다. 벌써부터 빡셈의 기운이 느껴진다고요? 예, 이것때문에 지피티 불렀습니다.
토스트 메시지는 뭐냐면 사용자가 어떤 상호작용을 할 때 뿅 하고 튀어나오는 작은 메시지이다. 뭐 예를 들어서 보내기 버튼을 눌렀다면 보냈습니다 하고 뿅 나왔다가 사라지는 메시지. 토스트 메시지는 생성된 아이디를 복사하기 위해 버튼을 눌렀을 때 복사되었습니다!가 뜬다. 그게 다다. 이건 지피티가 만들면서 동적으로 CSS 줘서 이렇게 된 거다. div로 만들고 따로 클래스를 안 줘서 저게 최선이더라...
그럼 이제 끝판왕 차례다.
복사버튼
function makeID() {
let idLength = idLengthval.value;
let idText = "";
let word_list = makelist();
for (let i = 0; i < idLength; i++) {
idText += word_list.charAt(Math.floor(Math.random() * word_list.length));
}
const idValue = document.createElement("p");
idValue.innerText = idText + " ";
const clipBoard = document.createElement("button");
clipBoard.innerHTML = `<i class="fa-solid fa-clipboard"></i> Copy`;
idValue.appendChild(clipBoard);
idValarea.appendChild(idValue);
clipBoard.addEventListener("click", () => {
navigator.clipboard.writeText(idText);
showToast("ID 복사 완료!");
});
}
벌써 뭐가 많다고요? 나도 그렇게 생각한다.
일단 위에도 썼듯이 아이디 생성기는 아이디를 만들때 p태그를 동적으로 생성하기때문에 HTML파일에는 아무것도 없다. 그래서 문제가 뭔데요?
function makeID() {
let idLength = idLengthval.value;
let idText = "";
let word_list = makelist();
let idValue = document.createElement("p");
//자바스크립트는 아스키코드 못다루나?
for (let i = 0;i < idLength;i++) {
idText += word_list.charAt(Math.floor(Math.random() * word_list.length));
idValue.innerText = idText;
idValarea.appendChild(idValue);
};
}
이전버전 코드를 한번 봅시다. 그러면 좀 하시는 분들은 뭐가 문제인지 바로 알 수 있다. 아니 그 속도 이런거 말고.
idValue가 생성된 아이디이고 얘를 그냥 다이렉트로 붙여버리기때문에 여기에 버튼을 만들어서 그냥 갖다 붙여버리면 에러가 나거나 버튼이 우리가 생각하는 그 버튼이 아닌 [HTML Object Button] 뭐 이런게 반긴다.
다시 이 부분을 보면 두번 붙이는 걸 알 수 있는데, idValue는 만들어진 아이디고 clipBoard는 복사버튼, idValarea는 생성된 아이디가 나열되는 div이다. 그니까 얘를 p태그 버튼 이렇게 병렬로 붙이는 게 아니라 버튼을 만들어서 p태그에 붙이고 그 p태그를 나열시킨 것. 이게 정말 빡셌음.
나머지는 크게 1. 아무것도 선택 안 했을 때 알림 뜨게 하는 것, 2. 뭔진 모르겠지만 좀 더 빨라진 정도...
오늘 해볼 건 그거다. 일반적으로 복사할때는 컨트롤씨 컨트롤브이를 눌러야 하는데, 가끔 다른 앱이나 사이트같은 거 이용하다 보면 클릭하면 복사가 되거나 옆에 있는 버튼을 누르면 복사되는 뭐 그런 게 있다. 그걸 해 볼거다.\
그냥 이렇게 만들었음. 여기서 뭘 해볼거냐면 위에다가 아무거나 텍스트를 쓰고 저 클립보드 아이콘을 누르면 텍스트 상자에 입력한 게 복사가 되게 할 거다.
const Text = document.querySelector('#text');
const copyButton = document.querySelector('#button');
어디갑니까 가져와야지.
우리야 컨트롤씨 컨트롤브이 하면 다 되니까 복붙은 쉽다고 생각할 수 있지만, 사실 복붙을 하려면 컴퓨터 입장에서는 클립보드에 적어뒀다가 그걸 붙여넣어야 한다. 그러니까 Ctrl+C 없이 저 버튼으로 복사가 되려면 저 버튼을 눌렀을 때 저 텍스트 상자 안에 있는 걸 클립보드로 기록해야 한다. 그리고 위 사진에 있는 게 클립보드다. (윈도우 기준으로 win키+v 누르면 열림)
여기까지 하면 된 거 맞는데, 문제가 하나 있다. 사용자들 중에 컴찐잘알이 몇이나 있겠음? 솔직히 여기 들어와서 왼도우키+v 누르면 클립보드가 나오는구나 한 분들도 계실 거 아니예요. 그리고 잘알 여부와 상관없이 복사하고 아무것도 안 뜨면 이게 된건지 아닌건지 몰라요. 그죠?
참고로 괴담수사대에 적용된 ::selection은 배경색이 #ff0000이 아닌데, #ff0000으로 하면 블록 긁다가 여러분들 눈뽕 옵니다.
어제 꼭두새벽에 뜬금없이 생각난 게 하나 있는데, 바로 ::selection을 글의 특정 비율(50% 이런 식으로 기준을 정해서)을 정해서 색을 나눌수 있을까였다. 그러니까 그 아수라백작처럼 말이다.
일단 결론부터 말하자면 특정 부분'부터' 블록을 씌웠을 때 색을 다르게 하는건 자바스크립트를 이용한 동적제어를 통해 가능하지만, 아예 블록을 씌웠을때 반반무마니가 되게 하는 건 자바스크립트로도 불가능하다. 아니, 사실 가능은 한데 약간 O(n)이 n!일때마냥 존내 비효율적이라 걍 CSS단에서 클래스 주는 게 낫단다. 이거 채찍피티한테 물어봄.
일단 모바일로 씌웠을때는 안보이고 PC에서 마우스로 긁어야 되긴 한데, 문단에 따라 마우스로 긁었을 때 색깔이 다르게 적용된 것을 볼 수 있다. 클래스를 다르게 주고 CSS단에서 색깔만 다르게 준 것이다. 이를 응용하면 특정 문장이나 문단을 블록 씌운 상태에서도 강조할 수 있다. 굳이 이렇게까지 할 필요가 있을지는 모르겠지만.
우리가 해 볼 게 위 그림 두 장에 나와있다. 머스크가 인수해서 똥을 싸제끼고 있는 트위터나 마스토돈, 블루스카이에는 한 번에 올릴 수 있는 내용에 글자수 제한이 있다. 트위터는 140자, 마스토돈은 500자(하이퍼스페이스 인스턴스), 블루스카이는 300자.
오늘은 입력창의 글자수를 실시간으로 세 보는 걸 할 건데, 해볼 게 총 두가지이다. 첫번째는 그냥 입력창에 몇 글자가 입력됐는지를 세 주는거고, 두번째가 위 두 장의 사진에 나와있는 n자 제한에서 n자가 남았는지 표시해보는 것이다. 아, 저 원은 아쉽게도 구현을 못 한다. 내 능력 밖임.
입력창의 글자 수를 나타내보자
그러니까 일단 남은 글자수는 모르겠고 몇 글자 썼는지 실시간으로 띄우는 것 먼저 해 보자. 얘를 해야 밑에께 좀 더 쉬워진다. 뭐 해요, VScode 키셔야지.
여기서 h1만 뭐가 없고 밑에 textarea랑 p는 클래스나 아이디가 하나씩은 있다. 왜? JS에서 갖고와야 하니까요.
그렇게 생각하던 시절이 저에게도 있었습니다. 아니 코드가 진짜로 저게 다다. 그리고 우리가 여기서 봐야 할 부분은 이벤트 리스트너밖에 없다. 진짜 이게 다예요? 네! 이게 답니다! 나도 내 두 눈을 의심했다니까.
여기 보이죠? 텍스트 입력하니까 21자라고 나오는 거.
지금까지 여기서 했던 코딩들을 보면 이벤트 리스트너에 'click'이 들어가 있다. 그러니까 이 클릭은 뭐냐면 뭘 눌렀을 때 이벤트를 발동시켜라 이 얘기다. 근데 이번 코드에는 클릭이 아니라… keyup? 이게 뭐임?
이벤트 리스트너의 트리거 중에는 키업(keyup)과 키다운(keydown)이 있다. 각각 키보드를 놓을 때/키보드를 누를 때 이벤트를 발동시켜라 이건데 무슨 차이인지는 모르겠음… 아무튼 키보드를 누를 때 이벤트를 발동시켜서 저 네모 상자에 입력한 텍스트가 몇 글자인지 알려줘라~ 라는 것만 알아두시면 된다. 그럼 이제 다음걸로 넘어가보자.
남은 글자 수를 나타내보자
일단 이건 위에꺼랑 달리 단순히 몇 글자를 썼냐를 표시하는 게 아니라, 본문 맨 위에 있는 스크린샷처럼 제한 글자 수에서 남은 글자 수를 표시할거다. 예를 들어서 글자 수가 140자라면 아무것도 안 썼을 때 140부터 시작해야 한다. 이해 하셨죠?
이게 아까 그거인데, 총 21자 나온다. 그럼 우리는 최대 글자 수에서 저걸 뺀 만큼을 출력하면 된다. 그러니까 아까 그 코드에서 JS 코드의 이벤트 리스트너 부분을 수정하면 된다. 일단 여기서는 최대 글자 수를 200자로 정해보자.
일단 이 코드에는 두 가지 변경점이 있다. 하나는 위에도 썼듯이 최대 글자 수에서 현재 글자 수를 뺀 걸 출력하게끔 바꾼거고… 다른 하나는 뭔가 하고 코드를 유심히 들여다 본 여러분은 뭔가 두 줄 추가된 것을 발견했다.
let maxtext = 200;
counter.innerText = maxtext;
엥? 이거 머임? 이거 왜 추가했어요? 얘가 없으면 페이지를 갓 띄웠을 때는 남은 글자수가 0으로 나온다. HTML코드에 0으로 되어있거든... 이게 뵈기 싫으면 아예 p태그나 span태그만 만들어놓고 innerText 박아버리는 방법도 있긴 하다. 대신 창이 열렸을 때 내용을 바꾸게 별도로 코딩은 해야겠지만.
그래서 남은 글자수만큼 제대로 표시해준다.
근데 또 여기서 끝내기는 뭔가 아쉽죠? 그래서 한가지를 더 해볼거다. 남은 글자 수에 따라서 저 밑에 숫자 색을 바꿔볼건데… 첫번째로는 전체 글자의 90% 이상을 썼을 때, 그러니까 10%가 남았을 때 주황색으로 바뀌게 할 거고, 두번째로는 글자 수가 전체 글자 수를 초과했을 때(음수가 될 때) 빨간색으로 바꿀거다.
뭐가 문제였냐면, 일단 inputarea.value.length는 현재 입력한 글자이다. 그러니까 저 변수를 안 건들고 조건을 제대로 설정하려면 현재 입력한 글자가 180자가 '넘어가야'한다. 왜? 그래야 남은 글자수가 20자 미만이 되니까. 그래서 그거 변경해줬더니 됐다.
남은 글자 수가 전체 글자 수의 10% 이상남은 글자 수가 전체 글자 수의 10% 미만
여기까지는 됐는데, 글자수가 오버됐을 때 빨간색으로 안 바뀌는겨… 진짜 아무리 해도 안바껴서 채찍피티한테 물어봤다.
조건문 우선순위 너무 복잡함... 아무튼 수정 포인트를 봅시다. 위 코드가 내가 짰던 코드인데, 보면 if문이 개같이 많아요. 그죠? 그걸 남은 글자 수가 10% 미만일때(200자 제한 기준 19자 이하 남았을 때)랑 남은 글자 수가 음수일때(글자수 초과했을 때) 두 가지로 나눴다. 그리고 우선순위를 올렸다는 건 무슨 얘기냐... if문을 잘 보자.
두 코드의 if문에서 over가 어디에 위치해있는지 확인해보자. 내가 쓴 코드는 over 클래스가 붙는 조건... 그러니까 글자 수가 제한을 넘겼을 때가 맨 밑에 있고, 채찍피티가 보완해 준 코드는 글자수가 제한을 넘겼을 때를 먼저 처리해준다. 이 위치 차이만으로도 적용되고 안되고가 갈린다. 정말 복잡시럽다.
남은 글자 수가 전체 글자 수의 10% 이상남은 글자 수가 전체 글자 수의 10% 미만글자수 오버