※이번 포스트로 부터 나온 최종 아웃풋
https://frontend-bear.tistory.com/86
이번엔 바닐라 자바스크립트로 MBTI 같은 테스트 페이지를 만들어보자.
작업 개요
- MBTI 테스트 처럼 OOOO 4가지로 구성된 결과가 나오는 테스트
- 객체를 이용해 만들어 확장성을 높인다.
작업 가이드 및 사담
PersonalTest 객체를 기반으로, 현재 진행상황, 선택된 값들의 정보
각각의 문항들을 만들고, 만들어진 HTML에 연결시킨다.
(테스트 시작 페이지 / 테스트 페이지 / 테스트 결과 페이지)
3페이지만 있으면 되고, DOM이 새로 생성되거나 하는일이 없어서
따로 스크립트로 DOM을 생성할 이유는 없어보인다.
그래도 메인 container의 클래스를 받아와서 여러개를 만들 수 있는 구조로 작업해보자.
이번작업 부터는 작업 내역을 git으로 올려서 공유할 생각이다.
음.. 포스팅을 쓰는건 좋은데 다시 읽어보니 좀 난잡한 부분이 있어서
최대한 간결하게(?) 읽기 편하게 작성하도록 노력해봐야지.
여튼 시작!
작업 진행
이번엔 JS부터 작업 해보자.
/* personalTest.js */
class PersonalTest {
constructor(target) {
this.container = document.querySelector(target); // 추후 dom 내용을 바꾸기 위한 선택자
this.page = 0; // 0: intro, 1: test, 2: result 현재 페이지
this.progress = 0; // 현재 질문 단계
this.questions = []; // 질문 모음
this.results = []; // 사용자가 선택한 답모음
}
init() {} // 추후 초기화가 필요한 경우 작성
getCurrentQuestions() { // 현재 progress의 질문을 반환
return this.questions[this.progress];
}
submitAnswer(answer) { // 사용자가 선택한 답을 results에 추가
this.results.push(answer);
this.progress++; // 질문 단계 증가
}
render() {} // 추후 dom에 내용을 바꾸기 위한 함수 작성
}
그리고 간단한 html파일을 추가해보자.
<!-- index.html -->
<!DOCTYPE html>
<html lang="kr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>개인 성향 검사</title>
<script src="./js/personalTest.js"></script>
</head>
<body>
<div class="personalTest_container"></div>
<script>
const personalTest = new PersonalTest('.personalTest_container');
</script>
</body>
</html>
여기까지 작업 후 index.html파일을 실행시키거나, vs코드의 Live server를 이용하여 크롬창을 띄울 수 있는데,
그 다음 개발자 도구를 열어 콘솔(console)
탭을 눌러 여기서 테스트를 하면서 진행을 해보자.
콘솔탭에 personalTest를 입력하면, 해당 객체가 나온다.
계속 바뀌어야 하는 변수를 확인해야 한다면, 일일히 콘솔창에 변수를 입력하기 보다,실시간 표현식
기능을 활용해보자.
상단의 눈모양을 클릭한 뒤 생기는 입력칸에 원하는 변수명을 입력하면 된다.
다시 눈모양을 클릭해서 여러개를 입력할 수도 있다.
이렇게 설정을 해두면 변수가 바뀔때마다 실시간으로 변경되는 값이 노출되므로 편리하다.
임시로 질문 두개정도를 넣어보자.
질문의 모양은
{
question: '나는 혼자있을때 더 편안함을 느낀다.',
answer: {y: '그렇다', n: '아니다'}
}
이런식으로 진행할 예정이고,
생각해보니까 OOOO의 형태니까 PersonalTest
의 questions
은 최종적으로
questions = {
EI: [
{
question: '나는 혼자있을때 더 편안함을 느낀다.',
answer: {a: '그렇다', b: '아니다'}
}
],
SN : [
{
question: '재밌는 영화를 보고 난 후 나는...',
answer: {a: '재밌었다. 밥이나 먹으러 가야지', b: '인터넷에서 영화 해석을 검색해본다.'}
}],
TF : [
{
question: '누군가 나를 싫어하는 걸 알았을 때.',
answer: {a: '어쩌라는건지', b: '왜 나를 싫어할까?'}
}],
JP : [
{
question: '나는 과제를 할 때',
answer: {a: '계획부터 세운다.', b: '일단 시작한다.'}
}]
}
이런 모양이 될것이다.
최종적으로, 예를 들어 E와 I를 판단한다고 하면,
EI 배열의 질문중 a의 값이 많다면 E, b의 값이 많다면 I 이런식으로 구성할 것이다.PersonalTest
의 constructor() 내부 this.questions에 위 질문들을 넣어주었다.
기존에 getCurrentQuestions()
는 questions가 배열인 경우를 생각해서 만들었던거니,
questions를 배열로 변환해보자.getQuestion()
라는 메서드를 만들건데, 해당 메서드는 questions를 배열로 바꾸고,
각각 질문의 내용에 type이라는 속성을 추가해서 어떤 항목인지 넣어줄 예정이다.
getQuestion() { // questions의 키를 참조해서 질문을 반환
return Object.keys(this.questions)
.flatMap(key => this.questions[key].map(question => ({ ...question, type: key })));
}
그리고 브라우저 콘솔창에서 personalTest.getQuestion();
를 입력하면 배열로 만들어진 값들이 나온다.
그리고 getCurrentQuestions()
의 내용도 바꿔주자.
getCurrentQuestions() {
// 현재 progress의 질문을 반환
return this.getQuestion()[this.progress];
}
그러면 이제, getCurrentQuestions()
메서드를 실행하면 현재의 질문을 받아올수있다.submitAnswer(answer)
메서드로 답을 제출한 뒤,
다시 getCurrentQuestions()
메서드를 실행 시키면 다음 질문이 나온다.
submitAnswer(answer)
메서드를 몇번 더 작동 시킨 뒤,getCurrentQuestions()
메서드를 작동 시키면 질문이 없으니 undefined
가 노출되게 된다.
그 뒤 personalTest.results;
를 찍어보면,['그렇다', '재밌었다. 밥이나 먹으러 가야지', '어쩌라는건지', '계획부터 세운다.']
이런식으로 값이 나오는걸 확인 할 수 있다.
그러면 지금 해야할일은
- 다음 질문이 없을때
submitAnswer(answer)
가 작동하지 않도록. (예외처리) - results 의 값이 저런식으로 찍히면 나중에 내용파악을 위한 추가 함수가 필요하니, 정리해서 결과값 입력하기
submitAnswer(answer)
이후getCurrentQuestions()
가 바로 시행되게 하기.getQuestion()
메서드가 여러곳에서 활용되니init()
안으로 넣어 객체 속성으로 지정하기.- 콘솔 내 확인용도로
start()
메서드 만들기.
위 내용을 추가한 코드
init() {
this.questionArray = this.getQuestion(); // 질문을 배열로 저장
}
start() {
if(this.progress !== 0) return; // 진행중이면 실행하지 않음
console.log(this.getCurrentQuestions()) // 브라우저 개발자 도구에 log 출력 용도
return this.getCurrentQuestions();
}
getQuestion() { // questions의 키를 참조해서 질문을 반환
return Object.keys(this.questions)
.flatMap(key => this.questions[key].map(question => ({ ...question, type: key })));
}
getCurrentQuestions() { // 현재 progress의 질문을 반환
return this.questionArray[this.progress];
}
submitAnswer(answer) {
if(this.questionArray.length <= this.progress){ // 질문이 끝났으면
console.log('더이상 질문이 없습니다.');
console.log(this.results)
return;
}
this.results.push({
type: this.questionArray[this.progress].type,
answer: Object.keys(this.questionArray[this.progress].answer)
.find(selectedAnswer => {
return this.questionArray[this.progress].answer[selectedAnswer] === answer;
})
}); // 사용자가 선택한 답을 results에 추가 (type: 질문의 키, answer: 사용자가 선택한 답의 키)
this.progress++; // 질문 단계 증가
return this.getCurrentQuestions();
}
render() {} // 추후 dom에 내용을 바꾸기 위한 함수 작성
잊지말고 constructor()
최하단에 this.init(); 을 추가해주고,
index.html 파일에서 생성자 호출 이후 personalTest.start();를 해주자.
그런 뒤 콘솔에서 personalTest.submitAnswer()
메서드를 연속으로 실행 해주면,
question에 id값이 있다면 좋겠지만.. 나중에 질문 추가 혹은 제거 등의 배열이 수정되는 경우가 발생하면
id가 중복이 되서 조금 불안하지만 질문과 답변의 text를 기반으로 선택하고 있다.
이제 calcResult()
라는 메서드를 만들어볼껀데,
이건 results 값에서 EI, SN, TF, JP 등 타입별 답변에 a,b가 각각 몇개인지 계산해주는 메서드다.
calcResult() {
return this.result = Object.keys(this.questions).reduce((acc, cur) => {
acc[cur] = this.results.filter(result => result.type === cur).reduce((acc, cur) => {
acc[cur.answer] = acc[cur.answer] ? acc[cur.answer] + 1 : 1;
return acc;
}, {});
return acc;
}, {});
}
result 값을 받아서, reduce()
를 이용해 각각 a와 b의 갯수를 구한 뒤 반환해준다.
각 항목마다 질문을 복사해서 여러개를 만들고 테스트를 해보자.
이제 찍히는걸 확인했으니 성향을 만들어줘야하는데,createPersonalResult()
라는 메서드를 새로 작성할꺼고,
규칙은 아래와 같게 작성해보자.
- a가 더 많으면 앞부분의 영어, b가 더 많으면 뒷부분의 영어
- a와 b의 값이 같으면 별표를 넣어줄 생각이다.(중립)
createPersonalResult(totalResult) {
return Object.keys(totalResult).reduce((acc, cur) => {
if(!totalResult[cur].a) return acc + cur[1]; // a가 없으면 b를 반환
if(!totalResult[cur].b) return acc + cur[0]; // b가 없으면 a를 반환
if(totalResult[cur].a === totalResult[cur].b){ // a와 b가 같으면
acc += '★';
return acc;
}
if(totalResult[cur].a > totalResult[cur].b){ // a가 b보다 크면
acc += cur[0]
}else{ // b가 a보다 크면
acc += cur[1]
};
return acc;
}, "");
}
이런식으로 작성해 주고, calcResult()
와 submitAnswer(answer)
를 조금 수정해주자.
submitAnswer(answer) {
if(this.questionArray.length <= this.progress){ // 질문이 끝났으면
console.log('더이상 질문이 없습니다.');
return `당신의 성향은 ${this.calcResult()}입니다.`;
}
this.results.push({
type: this.questionArray[this.progress].type,
answer: Object.keys(this.questionArray[this.progress].answer)
.find(selectedAnswer => {
return this.questionArray[this.progress].answer[selectedAnswer] === answer;
})
}); // 사용자가 선택한 답을 results에 추가 (type: 질문의 키, answer: 사용자가 선택한 답의 키)
this.progress++; // 질문 단계 증가
return this.getCurrentQuestions();
}
calcResult() {
const totalResult = this.result = Object.keys(this.questions).reduce((acc, cur) => {
acc[cur] = this.results.filter(result => result.type === cur).reduce((acc, cur) => {
acc[cur.answer] = acc[cur.answer] ? acc[cur.answer] + 1 : 1;
return acc;
}, {});
return acc;
}, {});
return this.createPersonalResult(totalResult); // 결과를 createPersonalResult에 넘겨줌
}
그리고 테스트를 해보면..
요런식으로 잘나오게 된다!
다음 포스트에선 여태 만든 객체를 가지고 html에 화면을 구현해보자.