들어가기 앞서
개발에 관심을 가지게 되면서 제일 먼저 만들어보고 싶었던 게 MBTI 테스트였다.
이걸 만들기 위해서는 먼저 HTML, CSS, JS가 뭔지 알아야 했고 그걸 알아가는 과정에서 코딩에 흥미가 생겼다.
어느 정도 지식이 생겼고 이제는 강의를 봐도 알아 들을 수 있을 것 같아 인프런에서 MBTI 강의를 수강했다. (무려 무료!)
멋사에서도 관련된 강의가 있는데, SEO랑 광고 붙이는 방법도 알려주지만 유료여서 그 부분은 내가 따로 공부하기로 했다.
아무튼 아래 강의는 바닐라JS라서 약간의 하드코딩이 필요한데, 어떤 식으로 돌아가는지 로직을 이해할 수 있어서 좋았다.
[무료] [하루 10분|Web Project] HTML/JS/CSS로 나만의 심리테스트 사이트 만들기 - 인프런 | 강의
HTML / JS / CSS 만을 가지고 직접 심리 테스트 사이트를 만들고, 배포해보는 Project 사이트를 제작합니다., - 강의 소개 | 인프런
www.inflearn.com
1. 강의는 띠별 연애타입을 다뤘지만 띠보다 MBTI가 더 대중적인 니즈가 있다고 생각해 유형을 16가지로 늘렸다.
2. 점수를 계산하는 과정도 어떤 답을 골랐느냐에 따라 가감을 하여 4가지 부문(EI/NS/FT/PJ)별로 결과를 더해줬다.
3. 카카오톡 API / 다시 하기 / 링크 복사의 기능을 추가했다.
4. 태그 매니저를 추가해 데이터를 추적했다.
💝 결과물 💝
전생 테스트
나는 전생에 뭐였을까?
whatismypreviouslife.netlify.app
디자인
첫 화면에서는 별이 뱅글뱅글 돌아가고, START 버튼을 누르면 테스트가 시작된다.
질문은 총 12개로, 질문 당 2가지 답변 중 1가지를 고를 수 있다. 얼마나 진행 되었는지 상단의 진행바 색을 보고 알 수 있다.
결과 페이지는 제목 / 이미지 / 좋아하는 것 / 싫어하는 것 / 잘 맞는 유형 / 잘 맞지 않는 유형을 보여주며,
버튼은 다시 하기 / 카카오톡 공유하기 / 링크 복사 3가지로 구성했다.
코드
화면은 크게 첫 화면 / 테스트 화면 / 결과 화면 3가지로 구성되어 있다.
1. 첫 화면
- 테스트 제목 / 설명을 적고 이미지에 CSS 효과로 rotate 사용
#first img {
animation: rotateImg 3.5s linear infinite;
}
@keyframes rotateImg {
100% {
transform: rotate(360deg);
}
}
- START 버튼 클릭 시 start() 실행되도록 onClick 사용 && 디자인으로 부트스트랩 사용
function start() {
first.style.display = "none";
qna.style.display = "block";
q.innerText = qnaList[0].q;
answer[0].innerText = qnaList[0].a[0].answer;
answer[1].innerText = qnaList[0].a[1].answer;
}
start() 함수는 첫 화면을 none으로 바꾸고, qna화면이 block으로 보이게끔 display를 수정한다.
첫 화면이 none으로 바뀌면 첫 화면에 구성되어 있던 요소들(제목, 돌아가는 별 이미지 등)이 모두 보이지 않게 되고,
아래 코드에 적어둔 qna화면의 구성 요소들이 보여진다.
2. 테스트 화면
- questionBox에 질문 순회 / answerBox에 답변 2가지 순회
<section id="qna" class="pt-4">
<div class="status mx-auto mt-5">
<div class="statusBar"></div>
</div>
<div class="questionBox mx-auto py-5 my-5 px-4"></div>
<div class="answerBox">
<div class="answer mx-auto py-3 my-2 px-4"></div>
<div class="answer mx-auto py-3 my-2 px-4"></div>
</div>
</section>
start()를 실행하면 questionBox (p)에 질문이 보여진다.
qnaList라는 배열에 q:질문, a:[{답1:답}, {답2: 답}] 이런 형태의 객체를 사전에 12개 준비했다.
q.innerText = qnaList[0].q 는 즉 질문 박스에 인덱스 0의 질문이 보여지는 것이고,
answer[0].innerText = qnaList[0].a[0].answer 이란 인덱스 0의 질문의 0번째 답이 보여진다는 뜻이다. (답1:답)
기본 구조는 순서에 맞게 질문과 답이 나오는 형태다.
어떤 답을 클릭하든 질문은 1씩 더해져야 하고, 질문이 12까지 도달하면 결과가 보여져야 한다.
const q = document.querySelector(".questionBox");
const answer = document.querySelectorAll(".answer");
let num = 1;
for (let i = 0; i < answer.length; i++) {
document
.querySelectorAll(".answer")
[i].addEventListener("click", function () {
if (num !== qnaList.length) {
q.innerText = qnaList[num].q;
answer[0].innerText = qnaList[num].a[0].answer;
answer[1].innerText = qnaList[num].a[1].answer;
num++;
}
plusStatusBar();
calScore(i);
});
}
처음 START 버튼을 눌렀을 때 0번째 질문과 0번째 질문에 대한 답1, 답2가 보여졌으므로
해당 반복문을 돌 때에는 1부터 시작해야 하니 num을 1로 선언했다.
answer의 길이는 언제나 2로, 어떤 답을 고르든 이벤트 리스너는 작동한다.
num이 마지막 질문 (12번)까지 간다면 순회를 마치고 calScore가 작동되며, 그 전까지는 계속 질문과 답을 순회한다.
더불어 어떤 답이든 클릭할 때마다 plusStatusBar()가 실행되어 상단의 진행바도 똑같이 채워진다.
- 진행바 구현
function plusStatusBar() {
let statusBar = document.querySelector(".statusBar");
statusBar.style.width = (100 / qnaList.length) * num + "%";
}
진행바의 가로는 100 / qnaList의 길이에 수를 곱한 값이다.
처음에는 0으로, 질문이 하나씩 진행되면 100에 가깝게 값이 계산된다. 이걸 %로 바꿔준다.
- 점수 계산
점수 계산은 EI / NS / FT / PJ = 0를 부여한 후 어떤 답변을 골랐느냐에 따라서 +1을 할지 -1을 할지 결정해 정했다.
질문은 총 12개로 EI(1~3), NS(4~6), FT(7~9), PJ(10~12)를 정하는데, 답1의 인덱스는 0이고, 답2의 인덱스는 1이었다.
클릭한 답의 인덱스가 0이라면 ENFP 계열에 1점을 더하고, 그렇지 않으면 1점을 뺐다.
// 점수 계산
let question = 1;
let EI = 0;
let NS = 0;
let FT = 0;
let PJ = 0;
function calScore(ans) {
if (question <= 3) {
EI = ans == 0 ? ++EI : --EI;
} else if (question <= 6) {
NS = ans == 0 ? ++NS : --NS;
} else if (question <= 9) {
FT = ans == 0 ? ++FT : --FT;
} else {
PJ = ans == 0 ? ++PJ : --PJ;
}
question++;
// 문제 수가 qnaList보다 커질 경우 결과 계산
if (question > qnaList.length) {
calResult(EI, NS, FT, PJ);
}
}
function calResult(EI, NS, FT, PJ) {
let result = "";
if (EI > 0) {
result += "E";
} else {
result += "I";
}
if (NS > 0) {
result += "N";
} else {
result += "S";
}
if (FT > 0) {
result += "F";
} else {
result += "T";
}
if (PJ > 0) {
result += "P";
} else {
result += "J";
}
showResult(result);
}
처음에는 E, I, N, S, F, T, P, J 총 8개 요소가 담긴 배열로 계산을 하려고 했는데
남자친구의 조언을 받아 조금 더 간결한 알고리즘으로 바꿨다. 이 편이 훨씬 깔끔한 것 같다.
예를 들어 모든 답을 1번째 답(인덱스 0)을 골랐다면 EI, NS, FT, PJ가 모두 3으로 0보다 큰 값이 될 것이고,
그러면 result에는 최종적으로 ENFP라는 문자열이 완성된다.
qnaList에서 질문/답을 구성했다면 infoList에서는 결과지를 구성했다.
infoList는 mbti, name, img_src, desc, like, hate, good, bad를 넣은 객체를 담은 배열이고,
result에서 만든 mbti와 infoList의 mbti를 매치시켜 최종 결과지를 보여주도록 만들었다.
이 부분에서 객체를 유용하게 써먹었다.
3. 결과 화면
- qna화면을 none하고 Showresult 화면을 block으로 변경
- result를 인수로 하여 결과 화면 구성
function showResult(result) {
qna.style.display = "none";
Showresult.style.display = "block";
let resultName = document.querySelector(".resultName");
let showImg = document.createElement("img");
let resultImg = document.querySelector(".resultImg");
resultImg.appendChild(showImg);
let resultDesc = document.querySelector(".resultDesc");
let resultLike = document.querySelector(".resultLike");
let resultHate = document.querySelector(".resultHate");
let resultGood = document.querySelector(".resultGood");
let resultBad = document.querySelector(".resultBad");
for (let i = 0; i < infoList.length; i++) {
if (result === infoList[i].mbti) {
showImg.src = `./img/${infoList[i].img_src}`;
showImg.style.width = "130px";
showImg.alt = `${infoList[i].mbti}`;
resultName.innerHTML = infoList[i].name;
resultDesc.innerHTML = infoList[i].desc;
resultLike.innerHTML = infoList[i].like;
resultHate.innerHTML = infoList[i].hate;
resultGood.innerHTML = infoList[i].good;
resultBad.innerHTML = infoList[i].bad;
}
}
}
결과지에 들어갈 요소가 많아서 약간 반복을 많이 했다.
결과 화면은 전체적으로 빈 공간을 만들고 거기에 정보를 하나씩 넣어주는 일이었다.
이미지의 경우 바로 삽입할 수가 없어 createElement로 img를 생성한 후, 그걸 품어주는 resultImg를 설정했다.
그외 결과 이름이나 설명, 좋아하는 것과 싫어하는 것, 잘 맞는 것과 잘 맞지 않는 것은 다 동일한 방법으로 공간을 만들었다.
infoList를 순회하면서 result와 infoList[i]의 mbit가 같다면 거기에 있는 정보들로 빈 공간을 채우게 한다.
이미지의 경우 src를 넣어줘야 하는데, 사전에 이미지를 저장할 때 mbit이름으로 저장해두었다.
(이미지도 다 구해왔는데 이게 매우 귀찮았다. 저작권도 걸리고... 이미지별로 크기가 일정하지 않아서 깨지고... 🥲)
또 사이즈가 다 달라 못생겨서, width를 js에서 설정해줬다. 이미지의 설명인 alt도 그 이미지의 mbti로 설정해주었다.
4. 버튼 구현 및 카카오톡 API
- 다시하기 버튼
function replay() {
setTimeout("location.reload(true)", 800);
}
다시하기 버튼은 클릭하면 replay() 가 실행되도록 했다.
이 함수가 실행되면 800밀리세컨즈 (0.8초) 후 location.reload가 true가 된다.
- 복사하기 버튼
function copy() {
let nowUrl = window.location.href;
navigator.clipboard.writeText(nowUrl).then(() => {
alert("링크 복사 완료!");
});
}
복사하기 버튼을 클릭하면 copy() 가 실행된다.
현재 url은 테스트 화면의 url, 즉 whatis... 어쩌고저쩌고인데, 이걸 복사하도록 했다.
클립보드 API라는데 클립보드에 해당 url을 복사하고, 콜백함수로 복사 완료를 알리는 얼럿이 뜨게 했다.
location이나 navigator 쪽 메서드도 더 알아봐야겠다. 재미있는 게 많을 것 같다.
- 카카오톡 API
const url = "https://whatismypreviouslife.netlify.app/";
function setShare() {
var resultImg = document.querySelector(".resultImg img");
var resultAlt = resultImg.alt;
const shareTitle = "전생 테스트";
const shareDesc = document.querySelector(".resultName").textContent;
const shareImage = url + "./img/" + resultAlt + ".jpeg";
const shareURL = url + "page/result-" + resultAlt + ".html";
Kakao.Link.sendDefault({
objectType: "feed",
content: {
title: shareTitle,
description: shareDesc,
imageUrl: shareImage,
link: {
mobileWebUrl: shareURL,
webUrl: shareURL,
},
},
buttons: [
{
title: "결과 확인하기",
link: {
mobileWebUrl: shareURL,
webUrl: shareURL,
},
},
],
});
}
카카오톡 공유하기 버튼을 누르면 setShare() 가 실행된다.
카카오톡 API는 공식 문서를 참조하면 되는데 나의 경우 강의에 있는대로 피드에 공유하는 형태로 만들었다.
아래 Kakao.Link.sendDefault가 카카오톡 API 구현을 위해 필요한 요소들인데,
제목, 설명, 이미지 url, 클릭하면 웹이나 모바일에서 이동될 링크, 버튼의 제목 등을 설정해야 한다.
이미지와 alt는 html을 참고해 적어뒀다.
여기서 제일 오류가 났던 부분은 shareDesc의 textContent다.
내가 적어둔 resultName 부분에는 <br>이 들어있었는데, innerHTML, innerText 뭘로 해도 오류가 났다.
자체에서 나는 게 아니라 카카오 API에서 오류가 나니까 해석도 안되고 미치고 팔짝 뛰는 줄 알았다.
덕분에 3가지 메서드의 차이점을 알 수 있었다.
innerHTML
- 요소의 HTML 내용을 설정하거나 가져오며 HTML 태그 / 텍스트 모두를 포함하고 HTML로 해석한다.
- 예를 들어 text.innerHTML = "안녕<br>하세요" 라고 하면 <br>이 보이는 게 아니라 줄바꿈으로 해석되어 보여진다.
innerText
- 위의 경우 <br>이 HTML 태그가 아닌 텍스트로 인지되어 그대로 다 보여진다.
textContent
- <br>을 해석하지 않는다.
- HTML은 HTML 태그를 태그로 반영하고, Text는 문자로 반영한다면 textContent는 텍스트만 가져간다.
해당 API에서는 textContent를 사용해 텍스트만 반영되도록 했다 ㅠㅠ...
이외에 결과 페이지는 16개를 모 두 만들었다... ^^
강의에서는 12개였는데 나는 스불재로 16개를... 만들었다.
크게 복잡한 건 아니고 오히려 자잘한 걸 계속 복사하고 붙여넣는 하드코딩이었다.
리액트로는 어떻게 구현하는 걸까...? 아무튼 바닐라JS보단 나았을 것 같다.
그렇게 공유할 url은 결과 페이지의 url로 만들어주면 카카오톡 메세지를 누를 때 거기로 랜딩된다.
그렇게 16개를 다 만들면 끝.
배포는 네틀리파이로 배포해봤는데 깃보다 훨씬 좋았다.
이름을 자유롭게 바꿀 수 있고 수정 배포도 순식간이고 깃의 그 심리적 장벽을 완화해줘서 그런가? ㅎㅎ...
5. 태그 매니저
마케팅 부트캠프에서 배웠던 구글 태그 붙이기도 해봤다.
5/9 부터 5/15 약 5일 간 약 600명이 테스트에 참여해주었다.
처음에 통계를 잘못 봐서 결과 페이지가 기니 scroll을 했다면 결과 페이지까지 갔다는 뜻이니,
실질적인 참여자라고 생각해서 참여한 사람들이 900명이 넘은 줄 알고 좋아했다.
그런데 다시 생각해보니 이 사람들이 중복으로 들어왔을 확률은 고려를 하지 못해서...
실제 중복되지 않는 쿠키로 계산하기 위해 사용자 수를 보니 실 사용자는 약 600명 정도였다.
그래도 이게 어디임......?!
우연히 인스타그램에 팍스가 리그램을 해주고, 팍스 팔로워 분이 적극적으로 결과지도 만들어주시고 공유를 해주셔서 홍보가 잘 된 것 같다.
시중 테스트에 비하면 조금 부족할 수도 있는데, 진심으로 재밌게 했다고 좋은 말씀도 해주시고 관심 가져주셔서 정말 정말 감사했다.
덕분에 무슨... 다른 나라에서도 참여하고... 당일 한 400-500명이 바로 해주신 것 같다.
졸업한지 오래고 별도 커뮤니티도 없어서 올린 곳이라곤 노마드코더와 인스타그램, 블로그 뿐인데 그래도 조금씩 타고타고 오셨나보다!
끝내는 말
이번 테스트를 만들면서 개발은 99%의 괴로움과 1%의 도파민으로 굴러간다는 생각을 했다.
원하는 기능을 구현하기까지 뇌를 쥐어 짜는 것 같이 힘든데, 또 만들어 놓고 나면 너무 재미있고 내 새끼 이뻐 죽겠고 그런다.
근데 왜 1% 밖에 없냐? 내 새끼를 계속 보고 있으면 미안하게도 자꾸 부족한 부분이 보이고 아쉽고 그런다.
그럼 또 내 새끼 내가 고쳐줘야 하고... 예쁘고 똑똑하게 만들어주려고 하다 보니 또 힘들고... 그런 것이다...
근데 그 과정이 너무 자학같은데 재미있었다. 중독성 있었다. 왜 안되는지 내 새끼 고쳐주고 싶고 답답하고...
점수 알고리즘 구현하는 거랑 카카오톡 API에서 몇 번 막혔는데 이 2일 동안 계속 코딩 꿈만 꿨다.
진짜 꿈에서 해결해서 눈 뜨자마자 해봤는데 ㅋㅋㅋ 안돼서 잠도 다 깨고...
다른 건 몰라도 이런 게 적성이라고 하는 거면 다행이었다. 적성에 맞는 것 같아서...
계속 나를 시험하고 시험했는데, 생각보다 적성에 맞아서 안도감을 느꼈다. 이 과정이 너무 힘들었으면 못 했을 것 같다.
감정적인 이야기는 여기서 그만하고, 그래서 이번에 만들어보면서
- 리액트를 배워야겠다! 이제 다음 스텝으로 넘어가자! 라는 생각을 했다.
- DB가 뭔지 궁금해졌다. 원래 접속하면 ~000명이 참여한 테스트~ 처럼 실시간 방문자를 반영하고 싶었는데, 남자친구 말로는 DB를 알아야 한다고 한다. 데이터 베이스인건 아는데 그게 그래서 어떻게 쓰이는 건지, 궁금했고 구현해보고 싶었다. (물론 3자릿수라 하기도 민망해 이 부분은 포기했지만ㅋㅋ)
끝!
'🛠️ 프로젝트 > 🧸 토이 프로젝트' 카테고리의 다른 글
[HTML + CSS] 네이버 로그인 폼 클론 코딩 (0) | 2023.07.07 |
---|---|
[바닐라JS 토이프로젝트] 가위바위보 게임 (3) | 2023.04.14 |