이제 Router를 사용해보도록 하겠습니다.

 

Component를 갈아껴주면서 SPA를 MPA처럼 보이게 하려면 Router가 필수적이죠

 

Home, About 컴포넌트를 만들어 이리저리 이동해보도록 하겠습니다.

 

npm install react-router-dom 을 다운로드 받습니다.

react-router-dom 6버전이 다운로드 받아졌네요

 

react-router-dom은 Router 안에 Routes 컴포넌트안의 Route들도 페이지를 변경합니다.

 

바로 코드를 보시겠습니다.

 

import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';

import Home from './Home';
import About from './About';
import Navigation from './Navigation'
function App() {
  return (
    <Router>
      <Navigation />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  );
}

export default App;

react-router-dom 의 BorwserRouter에서 Router와 Route그리고 Routes를 꺼냅니다.

 

Router는 컴포넌트가 갈아껴질 컨테이너고, Routes 윗 부분은 고정, 안은 갈아껴지는 컴포넌트들이 들어가면 됩니다.

 

이제 Home 컴포넌트와 About컴포넌트 Navigation 컴포넌트를 직접 만들어 넣어보세요

 

 

path와 함께 페이지가 슉 슉 변하는 것을 확인할 수 있습니다.

 

 

 

 

 

 

'프론트엔드 > React' 카테고리의 다른 글

React 3 - Form Input 다루기  (0) 2023.06.23
React 1 - React 빠르게 복습하기 - Hello world  (1) 2023.06.23

React에서 값들을 동적으로 다루기 위해서는 state를 이용해야합니다.

 

무슨 소리냐 하면, Input 태그를 만들어서 그냥 값을 입력하려고 해도 그 값이 state가 아니라면 실시간으로 변수에 읽어들일 수 없다는 걸 의미합니다.

 

input태그를 다루기 위해선 onChange 이벤트를 이용해서 setState를 실행시켜 state값에 실시간으로 들어오는 값들을 바꿔주어야 합니다.

 

FormExample.js를 하나 만들어보도록 하겠습니다.

import React from 'react'
class FormExample extends React.Component{

    constructor(){
        super();
        this.state = {
            name:'',
            email:'',
        }
    }

    render(){
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    Name :
                    <input type="text" name="name" value={this.state.name} />
                </label>
                <br />
                <label>
                    Email : 
                    <input type="email" name="email" value={this.state.email} />
                    <br />
                    <button type="submit">Submit</button>
                </label>
            </form>
        )
    }
}

export default FormExample

이렇게 만들고 input값에 아무리 값을 넣어도 state에 값이 실시간으로 들어가질 않습니다.

 

input태그가 작동하질 않으니 onChange를 이용해서 state값을 실시간으로 바꿔주는 handleChange 함수를 만들어보겠습니다.

  handleChange(event) {
        this.setState({ [event.target.name]: event.target.value });
      }

이 함수를 추가해주고

 

 <label>
                    Name :
                    <input type="text" name="name" value={this.state.name} onChange={this.handleChange}/>
                </label>
                <br />
                <label>
                    Email : 
                    <input type="email" name="email" value={this.state.email} onChange={this.handleChange}/>
                    <br />
                    <button type="submit">Submit</button>
                </label>

이렇게 onChange 이벤트에 해당 함수를 넣어주면 되겠습니다.

 

하지만 이렇게 하면 오류가 발생합니다.

 

왜일까요?

 

hnadleChange안에서 사용한 this는 누가 될까요? 이벤트가 일어난 노드가 대상이 됩니다.

 

대상 노드에겐 당연히 setState가 존재하질 않죠. 그렇기 때문에 undefined 에러가 납니다.

 

이 에러를 해결하기 위한 방법은 2가지가 있는데,

 

하나는 해당 함수의 this의 바인딩을 바꿔주는 것,

 

두번째는 함수를 화살표 함수로 바꿔주는 것입니다.

 

간단하게 화살표 함수로 바꿔주도록 하겠습니다.

 

import React from 'react'
class FormExample extends React.Component{

    constructor(){
        super();
        this.state = {
            name:'',
            email:'',
        }
    }
    handleChange = (event) => {
        this.setState({ [event.target.name]: event.target.value });
      }
  

    render(){
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    Name :
                    <input type="text" name="name" value={this.state.name} onChange={this.handleChange}/>
                </label>
                <br />
                <label>
                    Email : 
                    <input type="email" name="email" value={this.state.email} onChange={this.handleChange}/>
                    <br />
                    <button type="submit">Submit</button>
                </label>
            </form>
        )
    }
}

export default FormExample

자 이렇게 해주면 state값과 연동되는 input을 만들었습니다.

 

내친김에 handleSubmit도 만들어보도록 하죠

 

import React from 'react'
class FormExample extends React.Component{

    constructor(){
        super();
        this.state = {
            name:'',
            email:'',
        }
    }
    handleChange = (event) => {
        this.setState({ [event.target.name]: event.target.value });
      }

    handleSubmit = (event) =>{
        event.preventDefault();
        const {name, email} = this.state;
        console.log('Name : ', name)
        console.log('Email : ', email)

    }
  

    render(){
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    Name :
                    <input type="text" name="name" value={this.state.name} onChange={this.handleChange}/>
                </label>
                <br />
                <label>
                    Email : 
                    <input type="email" name="email" value={this.state.email} onChange={this.handleChange}/>
                    <br />
                    <button type="submit">Submit</button>
                </label>
            </form>
        )
    }
}

export default FormExample

handleSubmit 함수를 만들어 name과 email을 console에 찍어보았습니다.

 

이제  input태그 안의 값들을 수정하고 주고받을 수 있겠군요!!

'프론트엔드 > React' 카테고리의 다른 글

React 4 - Router 만들어보기  (0) 2023.06.23
React 1 - React 빠르게 복습하기 - Hello world  (1) 2023.06.23

1. node와 npm을 다운로드 합니다.

 

node와 npm은 리액트 프로젝트를 하기 위한 기초 단계입니다.

 

2. npx create-react-app app이름

 

터미널을 열어 해당 커맨드를 입력하여 react-app을 다운로드 만듭니다.

vue를 할 때 처럼 리액트도 마찬가지로 npx create-react-app 을 입력만 해주면 간단하게 프로젝트가 만들어집니다.

 

vue는 App 컴포넌트 아래 다양한 컴포넌트들을 추가하면서 프로젝트를 만들어나갔는데요

 

react도 비슷한 구조입니다.

 

일단

 

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

root는 reactDOM 객체의 루트가 되고 그곳에 App컴포넌트들 rendering한다고 되어있습니다. root는 id가 root인 div인데요

 

index.html에 보면 id가 root인 div가 있습니다. 이 곳에 render를 해주고, 컴포넌트들을 갈아껴주면서 앱이 작동하는 구조라고 보면 되겠습니다.

 

React 프로젝트를 하기 위해선 Hello World를 얼른 찍어봐야할텐데요

 

빠르게 컴포넌트를 하나 만들어서 띄어보도록 하겠습니다.

 

src 폴더에 Hello.js를 하나 만들고 다음과 같이 적어줍니다.

import React from 'react';

class Hello extends React.Component {
  render() {
    return <h2>Hello, from a class component!</h2>;
  }
}

export default Hello;

이게 기본 구조라고 생각하시면 됩니다. React를 import해서 Hello 클래스에 React.Component를 상속받아줍니다.

 

이제 이 클래스는 리액트 컴포넌트가 되었습니다.

 

render 함수에 반환 값으로 태그를 작성해줍니다.

 

그리고 다른 컴포넌트에서 가져다 써야하니 export default 값을 넣어줍니다.

 

이제 이 컴포넌트를 App에서 불러줍시다.

 

import logo from './logo.svg';
import './App.css';
import Hello from './Hello';

function App() {
  return (
    <div className="App">
      
    </div>
  );
}

export default App;

리액트의 컴포넌트에서는 class를 className으로 정의해줍니다. App에 Hello를 입포트해서 넣어줍시다.

 

그리고 해당 리액트 폴더로 이동해서 npm start를 해주면?

 

정상적으로 값들이 나오는 것을 확인할 수 있습니다.

 

리액트로의 발걸음을 드디어 뗐습니다.

'프론트엔드 > React' 카테고리의 다른 글

React 4 - Router 만들어보기  (0) 2023.06.23
React 3 - Form Input 다루기  (0) 2023.06.23

문제 페이지를 모두 작성하기는 했는데...

블로그를 작성하면서 동시에 만들어 보고 있어서 블로그에 차례 차례로 올리려다가 이것 저것 사소한 부분들을 하나 하나 고치다보니,

수정한 부분들이 너무 많아져서 그걸 블로그에 다시 재업로드 하기에 시간이 많이 들게 돼서.. 그냥 블로그는 잠시 올리지 않고 다 만들어 버렸습니다.

 

완성본

 

https://duljjii.github.io/

App.vue

더보기
<template>
  <div id="app" class="container fw-bold">
    <QuestionComponent
      v-for="(question, index) in questions"
      :key="index"
      :question="question.question"
      :answer="question.answer"
      :index="index"
      :total-count="questions.length"
      v-show="index == showIndex"
      :image="question.image"
      @next-move="nextMove"
      @prev-move="prevMove"
      @save-answer="saveAnswer"
    />
    <button
      class="submit-button"
      @click="submitAnswers"
      v-show="showIndex != -1"
    >
      Submit
    </button>

    <ResultComponent
      v-if="showIndex === -1"
      :correct-answer-count="correctAnswerCount"
      :total-count="questions.length"
      :wrongAnswerList="getWrongtAnswerList"
      :wrongQuestionList="getWrongQuestionList"
    />
  </div>
</template>

<script>
import QuestionComponent from "@/components/QuestionComponent.vue";
import ResultComponent from "@/components/ResultComponent.vue";
import _ from 'lodash';
export default {
  name: "App",
  data() {
    return {
      questions: [
        {
          question:
            "다음 중, Vue 인스턴스와 div를 연동하기 위해 빈칸에 넣어주어야 할 속성은?",
          answer: {
            a: "computed",
            b: "methods",
            c: "method",
            d: "el",
          },
          image: require("@/assets/problem1.png"),
          correctAnswer: "d",
        },
        {
          question:
            "html에 입력한 값이 달라지면 자동으로 Vue의 data 값도 바뀌도록 프로그램을 짜려고 할 때, input 태그 안에 넣어야 할 속성으로 알맞은 것은? ",
          answer: {
            a: "v-for",
            b: "v-model",
            c: "v-if",
            d: "v-bind",
          },
          image: require("@/assets/problem2.png"),

          correctAnswer: "b",
        },
        {
          question:
            "app 이라는 이름으로 만든 Vue 인스턴스 안에서 methods를 이용하여 message값을 바꾸려고 한다. 다음 중 methods를 잘못 정의한것은?",
          answer: {
            a: 'myFunction(){ this.message ="Hi"}',
            b: 'myFunction : function(){ this.message = "hi"}',
            c: 'myFunction : ()=>{ app.message = "hi" }',
            d: 'myFunction : ()=>{ this.message = "hi"}',
          },
          image: require("@/assets/problem3.png"),

          correctAnswer: "d",
        },
        {
          question:
            "다음 중, Vue 인스턴스의 watch 속성에서 오브젝트의 변화를 감지하기 위해 추가해야하는 속성은?",
          answer: {
            a: "handler",
            b: "deep",
            c: "set",
            d: "on",
          },
          image: require("@/assets/problem4.png"),

          correctAnswer: "b",
        },
        {
          question:
            "다음 중, Vue 인스턴스의 watch 속성에서 해당 data의 변화를 감지하면 실행되는 함수의 이름은?",
          answer: {
            a: "handler",
            b: "deep",
            c: "set",
            d: "handle",
          },
          image: require("@/assets/problem5.png"),
          correctAnswer: "a",
        },
        {
          question:
            "vue/cli 에서 style scoped를 주지 않고 스타일을 적용시켰을 때, 적용되는 범위로 알맞은것은?",
          answer: {
            a: "해당 컴포넌트에만 스타일이 적용된다.",
            b: "해당 컴포넌트와 부모 컴포넌트에만 스타일 적용된다.",
            c: "해당 컴포넌트와 자식 컴포넌트에만 스타일 적용된다.",
            d: "모든 컴포넌트에 스타일이 적용된다.",
          },
          image: require("@/assets/problem6.png"),
          correctAnswer: "d",
        },
        {
          question: "style scoped의 설명으로 옳지 않은것은?",
          answer: {
            a: "전역 스타일과 겹치는 스타일이 있더라도 해당 스타일이 적용된다.",
            b: "해당 컴포넌트에만 스타일이 적용된다 .",
            c: "부모 태그에게도 해당 스타일이 적용된다.",
            d: "스타일 태그의 적용 범위를 조절하기 위해 사용한다",
          },
          image: require("@/assets/problem7.png"),
          correctAnswer: "c",
        },
        {
          question: "다음 중 코드에 들어간 @는 어느 폴더를 가리키는가??",
          answer: {
            a: "dist",
            b: "public",
            c: "components",
            d: "src",
          },
          image: require("@/assets/problem8.png"),
          correctAnswer: "d",
        },
        {
          question:
            "특정 조건이 false일 때, display:none을 이용하여 화면 출력을 제어할 수 있는 속성은?",
          answer: {
            a: "v-if",
            b: "v-else",
            c: "v-bind",
            d: "v-show",
          },
          image: require("@/assets/problem9.png"),
          correctAnswer: "d",
        },
        {
          question:
            "특정 조건이 false일 때, 해당 요소를 HTML태그에서 제거하여 화면 출력을 제어할 수 있는 속성은?",
          answer: {
            a: "v-if",
            b: "v-else",
            c: "v-bind",
            d: "v-show",
          },
          image: require("@/assets/problem10.png"),
          correctAnswer: "a",
        },
        {
          question:
            "다음 코드와 같이 자식 컴포넌트에 props를 전달하고자 한다. html태그 안에 들어가야 할 속성의 case로 알맞은것은?",
          answer: {
            a: "camelCase",
            b: "kebab-case",
            c: "snake_case",
            d: "PascalCase",
          },
          image: require("@/assets/problem11.png"),
          correctAnswer: "b",
        },

        {
          question:
            "static prop을 이용하여 다음과 같이 값을 넘겨 주었을 때, 값의 type으로 알맞은것은?",
          answer: {
            a: "Object",
            b: "Number",
            c: "String",
            d: "Array",
          },
          image: require("@/assets/problem12.png"),
          correctAnswer: "c",
        },
        
        {
          question:
            "dynamic prop을 이용하여 다음과 같이 값을 넘겨 주었을 때, 자식 태그의 props 에서 받게되는 변수명의 이름은?",
          answer: {
            a: "passTotalCount",
            b: "totalCount",
            c: "total-count",
            d: "pass_total_count",
          },
          image: require("@/assets/problem13.png"),
          correctAnswer: "b",
        },

        {
          question:
            "dynamic prop으로 다음과 같이 값을 내려주었을 때, 해당 값의 타입으로 알맞은것은?",
          answer: {
            a: "Object",
            b: "Number",
            c: "String",
            d: "Array",
          },
          image: require("@/assets/problem14.png"),
          correctAnswer: "b",
        },
        {
          question:
            "버튼을 눌러 부모에게 받은 index값을 수정하려고 한다. 해당 메소드의 빈칸에 들어갈 값으로 알맞은것은?",
          answer: {
            a: "$store",
            b: "$event",
            c: "$on",
            d: "$emit",
          },
          image: require("@/assets/problem15.png"),
          correctAnswer: "d",
        },
        {
          question:
            "자식 객체에서 발생시킨 이벤트를 가져올 때, 컴포넌트의 html태그 안에서 @이후에 사용되는 case는?",
          answer: {
            a: "kebab-case",
            b: "camelCase",
            c: "PascalCase",
            d: "snake_case",
          },
          image: require("@/assets/problem16.png"),
          correctAnswer: "a",
        },
        {
          question:
            "다음 중 Vuex에서 Vue의 data와 비슷한 기능을 하는 속성으로, Vue 인스턴스의 상태를 정의하는 곳은?",
          answer: {
            a: "state",
            b: "mutations",
            c: "getters",
            d: "actions",
          },
          image: require("@/assets/problem17.png"),
          correctAnswer: "a",
        },
        {
          question:
            "다음 중 Vuex에서 state를 값을 변경하는 함수들을 정의하는 곳으로, 동기 함수들만 사용이 가능한 곳은?",
          answer: {
            a: "state",
            b: "mutations",
            c: "getters",
            d: "actions",
          },
          image: require("@/assets/problem18.png"),
          correctAnswer: "b",
        },
        {
          question:
            "다음 중 Vuex에서 state를 값을 변경하는 함수들을 정의하는 곳으로, 비동기 함수의 사용이 가능한 곳은?",
          answer: {
            a: "state",
            b: "mutations",
            c: "getters",
            d: "actions",
          },
          image: require("@/assets/problem19.png"),
          correctAnswer: "d",
        },
        {
          question:
            "다음 중 Vuex에서 state를 값을 변경하지 않고, state를 이용하여 계산된 식을 return해주는 함수를 정의하는 곳은?",
          answer: {
            a: "state",
            b: "mutations",
            c: "getters",
            d: "actions",
          },
          image: require("@/assets/problem20.png"),
          correctAnswer: "c",
        },
        {
          question: "mutations의 첫번째 함수에 들어오는 값은?",
          answer: {
            a: "state",
            b: "store",
            c: "context",
            d: "index",
          },
          image: require("@/assets/problem21.png"),
          correctAnswer: "a",
        },
        {
          question: "actions의 함수에 첫번째 인자로 들어오는 값은?",
          answer: {
            a: "state",
            b: "store",
            c: "context",
            d: "index",
          },
          image: require("@/assets/problem22.png"),
          correctAnswer: "c",
        },
        {
          question:
            "Vue 컴포넌트에서 Vue store에 있는 mutations를 사용하기 위해 사용해야하는 함수로 올바른것은?",
          answer: {
            a: "dispatch",
            b: "emit",
            c: "getters",
            d: "commit",
          },
          image: require("@/assets/problem23.png"),

          correctAnswer: "d",
        },
        {
          question:
            "Vue 컴포넌트에서 Vue store에 있는 actions를 사용하기 위해 사용해야하는 함수로 올바른것은?",
          answer: {
            a: "emit",
            b: "dispatch",
            c: "commit",
            d: "store",
          },
          image: require("@/assets/problem24.png"),

          correctAnswer: "b",
        },
        {
          question:
            "Vuex의 state 중 message를 Component에서 가져와 사용하려고 할 때, 올바르게 호출한 것은?",
          answer: {
            a: "this.store.state.message",
            b: "this.store.$state.message",
            c: "this.$store.$state.message",
            d: "this.$store.state.message",
          },
          image: require("@/assets/problem25.png"),

          correctAnswer: "d",
        },
        {
          question:
            "Vuex의 getters 중 messageLength라는 함수의 리턴값을 Component에서 가져와 사용하려고 할 때, 올바르게 호출한 것은?",
          answer: {
            a: "this.$store.getters.messageLength",
            b: "this.$getters.messageLength()",
            c: "this.$store.getters.messageLength()",
            d: "this.$store.getters('messageLength')",
          },
          image: require("@/assets/problem26.png"),
          correctAnswer: "a",
        },
        {
          question:
            "Vuex의 getters에서 해당 getters에 선언된 다른 함수를 사용하려고 한다. b 변수에 할당되는 값으로 알맞은것은?",
          answer: {
            a: "context",
            b: "state",
            c: "this",
            d: "getters",
          },
          image: require("@/assets/problem27.png"),
          correctAnswer: "d",
        },
        {
          question: "해당 코드에서 a변수에 할당되는 값으로 알맞은것은?",
          answer: {
            a: "this",
            b: "window",
            c: "state",
            d: "store",
          },
          image: require("@/assets/problem28.png"),
          correctAnswer: "c",
        },
        {
          question:
            "dynamic prop을 이용하여 다음과 같이 값을 넘겨 주었을 때, 값의 type으로 알맞은것은?",
          answer: {
            a: "Object",
            b: "Number",
            c: "String",
            d: "Array",
          },
          image: require("@/assets/problem29.png"),
          correctAnswer: "a",
        },
        {
          question:
            "다음 중 vue/cli에서 data를 정의하는 방법으로 옳은 것은?",
          answer: {
            a: "data : { message:'duljji' }",
            b: "data(){ {message : 'duljji'} }",
            c: "data(){ return message : 'duljji'}",
            d: "data(){ return { message : 'duljji'} }",
          },
          image: require("@/assets/problem30.png"),
          correctAnswer: "d",
        },
        {
          question:
            "다음은 v-for를 활용하여 li를 반복하는 코드이다. a, b, 에 들어오는 값과 c에 넣어야 할 속성으로 알맞은것은?",
          answer: {
            a: "a : key(index), b:value, c : :key ",
            b: "a : key(index), b:value, c : key",
            c: "a : value, b : key(index), c : :key",
            d: "a : value, b : key(index), c : key",
          },
          image: require("@/assets/problem31.png"),
          correctAnswer: "c",
        },
        {
          question:
            "Life Cycle Hooks 중 Vue 인스턴스가 생성될 때 실행되며, HTML태그에 접근할 수 없는 Hook은?",
          answer: {
            a: "created",
            b: "mounted",
            c: "updated",
            d: "destroyed",
          },
          image: require("@/assets/problem19.png"),
          correctAnswer: "a",
        },
        {
          question:
            "Life Cycle Hooks 중 Vue 인스턴스가 HTML태그에 등록될 때, HTML태그에 접근이 가능한 Hook은?",
          answer: {
            a: "created",
            b: "mounted",
            c: "updated",
            d: "destroyed",
          },
          image: require("@/assets/problem19.png"),
          correctAnswer: "b",
        },
        {
          question:
            "Life Cycle Hooks 중 Vue 인스턴스에 변화가 발생할 때 실행되는 Hook은?",
          answer: {
            a: "created",
            b: "mounted",
            c: "updated",
            d: "watch",
          },
          image: require("@/assets/problem19.png"),
          correctAnswer: "c",
        },
        {
          question:
            "다음 코드의 설명으로 옳지 않은 것은?",
          answer: {
            a: "cute 클래스는 vue data와 관계없이 무조건 적용된다.",
            b: "pretty 클래스는 isActive의 값이 true인지 false인지에 따라 적용 여부가 결정된다.",
            c: "style color는 vue data의 color값이 true인지 false인지에 따라 적용 여부가 결정된다.",
            d: "li 태그의 글자색은 빨간색이다",
          },
          image: require("@/assets/problem32.png"),
          correctAnswer: "c",
        },
      ],
      showIndex: 0,
      selectedAnswerList: [],
    };
  },


  components: {
    QuestionComponent,
    ResultComponent,
  },
  methods: {
    nextMove() {
      this.showIndex =
        this.showIndex + 1 > this.questions.length - 1
          ? this.questions.length - 1
          : this.showIndex + 1;
    },
    prevMove() {
      this.showIndex = this.showIndex - 1 < 0 ? 0 : this.showIndex - 1;
    },
    saveAnswer({ index, selectedAnswer }) {
      this.selectedAnswerList[index] = selectedAnswer;
      console.log(this.selectedAnswerList);
    },
    submitAnswers() {
      this.showIndex = -1;
    },
  },
  computed: {
    correctAnswerCount() {
      let correctAnswerCount = 0;
      this.selectedAnswerList.forEach((selectedAnswer, idx) => {
        console.log(selectedAnswer, this.questions[idx].correctAnswer);
        if (selectedAnswer === this.questions[idx].correctAnswer) {
          correctAnswerCount++;
        }
      });
      return correctAnswerCount;
    },
    getWrongtAnswerList() {
      const wrongAnswerList = this.selectedAnswerList.filter(
        (selectedAnswer, idx) => {
          return selectedAnswer != this.questions[idx].correctAnswer;
        }
      );
      console.log(wrongAnswerList);
      return wrongAnswerList;
    },
    getWrongQuestionList() {
      const wrongQuestionList = this.questions.filter((question, idx) => {
        return this.selectedAnswerList[idx] != question.correctAnswer;
      });
      console.log(wrongQuestionList);
      return wrongQuestionList;
    },
  },
  created() {
    this.questions = _.shuffle(this.questions)
    this.selectedAnswerList = new Array(this.questions.length).fill("");

  },
};
</script>

<style scoped>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  margin-top: 60px;
}

.container {
  background-color: #fce4ec;
  border-radius: 10px;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
  padding: 40px;
}

.question-text {
  font-size: 24px;
  color: #333333;
  margin-bottom: 20px;
}

.answer-label {
  font-size: 20px;
  color: #333333;
  margin-bottom: 10px;
}

.answer-input {
  margin-right: 10px;
}

.submit-button {
  background-color: #ff69b4;
  border: none;
  border-radius: 50px;
  color: white;
  font-size: 18px;
  padding: 10px 30px;
  margin-top: 30px;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
  transition: all 0.3s ease-in-out;
}

.submit-button:hover {
  background-color: #ff94c2;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.8);
  cursor: pointer;
}
</style>

QuestionComponent.vue

더보기
<template>
  <div>
    <div class="text-start">
      
      <h3 class="question-number">문제 {{ index + 1 }}</h3>
      <p class="question-text">{{ question }}</p>
      <div class="text-center">

      <img :src="image" alt="" class="img-fluid" />
      </div>
    </div>
    <div class="mt-3 container d-flex justify-content-center">
      <div class="options">
        <div
          class="mt-3 answerBox d-flex flex-column justify-content-between align-items-start"
        >
          <div class="d-flex mt-3">
            <p class="ms-2 mb-1">
              <input
                type="radio"
                v-model="selectedAnswer"
                :name="'answer' + index"
                value="a"
                :id="'a' + index"
              />
            </p>
            <div class="text-start me-2">
              <label class="ps-4" :for="'a' + index">{{ answer.a }}</label>
            </div>
          </div>

          <div class="d-flex">
            <p class="ms-2 mb-1">
              <input
                type="radio"
                v-model="selectedAnswer"
                :name="'answer' + index"
                value="b"
                :id="'b' + index"
              />
            </p>

            <div class="text-start me-2">
              <label class="ps-4" :for="'b' + index">{{ answer.b }}</label>
            </div>
          </div>
          <div class="d-flex">

          <p class="ms-2 mb-1">
            <input
              type="radio"
              v-model="selectedAnswer"
              :name="'answer' + index"
              value="c"
              :id="'c' + index"
            />
            <div class="text-start me-2">

            <label class="ps-4" :for="'c' + index">{{ answer.c }}</label>
            </div>
          </div>
          <div class="d-flex mb-3">

          <p class="ms-2 mb-1">
            <input
              type="radio"
              v-model="selectedAnswer"
              :name="'answer' + index"
              value="d"
              :id="'d' + index"
            />
            <div class="text-start me-2">

            <label class="ps-4" :for="'d' + index">{{ answer.d }}</label>
            </div>
          </div>
        </div>
        <div class="d-flex justify-content-between mt-3">
          <button
            @click="prevMove"
            class="btn next-btn"
            :class="{ invisible: isFirst }"
          >
            이전 문제
          </button>
          <button
            @click="nextMove"
            class="btn prev-btn"
            :class="{ invisible: isLast }"
          >
            다음 문제
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedAnswer: null,
    };
  },
  computed: {
    isFirst() {
      return this.index === 0;
    },
    isLast() {
      console.log(this.index, this.totalCount)
      return this.index === this.totalCount - 1;
    },
  },
  props: {
    question: String,
    answer: Object,
    index: Number,
    totalCount: Number,
    image:String,

  },
  methods: {
    nextMove() {
      this.$emit("next-move");
    },
    prevMove() {
      this.$emit("prev-move");
    },
    saveAnswer() {
      this.$emit("save-answer", {
        index: this.index,
        selectedAnswer: this.selectedAnswer,
      });
    },
  },


  watch: {
    selectedAnswer() {
      this.saveAnswer();
    },
  },
};
</script>
<style>
.question-number {
  color: #ff69b4;
  font-size: 24px;
  margin-bottom: 10px;
}

.question-text {
  color: #4b0082;
  font-size: 20px;
  margin-bottom: 20px;
  text-shadow: 2px 2px 3px #ffb6c1;
}

.options {
  background-color: #fffacd;
  border-radius: 10px;
  box-shadow: 2px 2px 10px #d2691e;
  padding: 20px;
}

.answerBox {
  border: 1px solid black;
  width: 300px;
  height: 300px;
  text-align: center;
  font-size: 1rem;
}

@media screen and (min-width: 768px) {
  .answerBox {
    width: 800px;
    height: 200px;
  }
 
}

.btn {
  background-color: #ff69b4;
  border: none;
  border-radius: 10px;
  color: #fff;
 
  font-weight: bold;
  margin-top: 20px;
  padding: 10px 20px;
  text-shadow: 2px 2px 3px #ffb6c1;
  transition: all 0.3s ease-in-out;
}

.btn:hover {
  box-shadow: 2px 2px 5px #d2691e;
  cursor: pointer;
  transform: scale(1.1);
}

.invisible {
  display: hidden;
}
</style>

Result.vue

더보기
<template>
  <div>
<p>

    총 {{ totalCount }}개의 문제 중 {{ correctAnswerCount }}개의 문제를 맞췄습니다
</p>

    <div class="d-flex flex-column align-items-start" v-if="totalCount!=correctAnswerCount">
      <h1 class="fw-bold">틀린문제</h1>
      <div>

      <ul>
        <li v-for="(wrongAnswer, index) in wrongAnswerList" :key="index">
          <p>
           {{index+1}}. {{ wrongQuestionList[index].question }}
          </p>
          <p>틀린 답 : {{ wrongQuestionList[index].answer[wrongAnswer] }} 정답 :
          {{
            wrongQuestionList[index].answer[
              wrongQuestionList[index].correctAnswer
            ]
          }}</p>
          
        </li>
      </ul>
      </div>
    </div>
    <div v-else>
        <h1 class="fw-bold">정답을 모두 맞추셨습니다 축하합니다!!!</h1>
    <img :src="oneHundredItem" alt="">
    <p>100점을 맞춘 뒤 나오는 귀여운 샴푸를 가장 먼저 보내신 분께 스타벅스 기프티콘을 드리겠습니다</p>
    </div>
  </div>
</template>

<script scoped>
export default {
  name: "MypjtResultComponent",

  data() {
    return {
      oneHundredItem : require("@/assets/oneHundredItem.png")
    };
  },
  props: {
    
    correctAnswerCount: Number,
    totalCount: Number,
    wrongAnswerList: Array,
    wrongQuestionList: Array,
  },

  mounted() {},

  methods: {},
};
</script>

<style>

ul{
    text-align: left;
}
li {
  list-style: none;
}
</style>

일단 image의 경로를 자식 컴포넌트에서 바로 넣어주지 않고 각 Question들이 image 경로 속성을 가지고 있게끔 만들었습니다.

 

그렇게 해야 문제를 섞을 때도, 해당 image들이 잘 들어갈 수 있기 때문입니다.

 

문제들을 섞게 되면 인덱스들의 값도 모두 바뀌기 때문에 더이상 QuestionComponent에서 인덱스에 해당하는 파일을 불러오는 것으로는 이미지를 매칭시킬 수 없었기에 문제들에 이미지경로를 직접 넣어줌으로써, 해당 문제의 이미지 속성을 가져오는 것으로 문제와 이미지를 매칭시켰습니다.

 

v-if 속성은 해당 조건이 만족되지 않으면 html태그에 나오지 않기 때문에 이스터에그를 넣기 아주 좋은 방법이라는 생각이 들었습니다.

 

100점을 맞추면 귀여운 강아지 사진이 나오도록 설정을 해두었습니다. 

 

역시 직접 사이트를 만드는 묘미는 이런 이스터에그를 내가 마음대로 넣고 빼고 할 수 있다는 점인 것 같습니다

 

문제 사이트를 직접 만들어 보면서 느낀점이 많은데, 

 

일단 개발을 하면서 재밌고 좋았던 것은 내가 '어.. 이 기능 있었으면 좋겠는데?' 라고 생각하는 는 것을 바로바로 넣어볼 수 있다는 점이었습니다.

 

SSAFY에서 배운 django와 DB들을 활용하여 문제집들을 추가적으로 활용하고 싶은 생각도 들었는데, 그렇게 서버까지 이용해서는 github으로 배포가 어려울 수도 있겠다는 생각이 들어, 잠시 접어두었지만요.

 

얼른 웹 서버를 이용한 배포를 하는 법도 익혀서 좀 더 재미있는 기능들을 넣은 웹페이지를 만들어보고 싶어 졌습니다.

 

계정을 만들어서, 각 계정들마다 문제를 풀면 경험치를 준다든지, 모든 사용자들에게 적용되는 공통 변수를 만들어서 가장 빨리 모든 문제를 푼 사람에게는 기프티콘이 나온다든지 하는 기능들을 구현할 수 있을테니까요

 

local storage를 활용하여 문제를 풀 때마다 경험치를 늘리는 기능도 만들어보긴 했지만, 30문제밖에 만들지 않아 의미가 없다 싶어 다시 뺐습니다.

 

아쉬운점은 Router를 활용하여 문제를 카테고리별로 나누는 것도 좀 더 공부하기 편한 사이트가 되지 않았을까 하는 마음이 들은 것입니다. 사실 컴포넌트별 props를 주어 출력되는 값들이 달라지게 만들었으니 , 그저 경로만 걸어주면 되는 간단한 기능인데, 문제 수가 없다보니 각 라우터 별로 4~5개의 작은 문제들이 나오는 것보다는 한번에 모든 문제가 나오는게 낫겠다는 판단이었는데,

 

직접 문제들을 만들어내다보니, 문제를 만드는 시간이 페이지를 구현하는 시간보다 더 오래 걸려, 다음 번에는 더미데이터를 활용하거나 API를 활용해서라도 좀 더 UX가 괜찮은 사이트를 만들어 보고 싶어졌습니다.

 

UI는... 좀 더 공부하도록 하겠습니다.

 

처음엔 모바일 화면에서 레이아웃이 모두 깨져, 어떻게 해야하나 고민이 있었는데, flexbox와 @media속성만 있으면 이제 반응형 웹페이지는 전혀 무섭지 않아졌습니다

 

아마 제가 혼자 공부하기 위해 만들어보자!! 라는 생각이었다면 이렇게 시간을 투자해서 페이지를 만들지 못했을텐데,  싸피 친구들이 시험을 잘 봤으면 좋겠다는 마음 덕에 의욕이 생겼던 것 같습니다.

 

그럼 다들 시험 잘 보기를 바라면서...!!

 

 

이제 정답을 제출하여 문제를 맞췄는지 틀렸는지를 체크해보도록 하겠습니다.

 

Submit 버튼에 @click='submitAnswers' 이벤트를 만들어두고, methods에서 submitAnswers를 정의해주면 됩니다.

 

   
<button class="submit-button" @click="submitAnswers">Submit</button>
 
 
submitAnswers() {
      let correctAnswerCount = 0
      this.selectedAnswerList.forEach((selectedAnswer, idx)=>{
        if(selectedAnswer == this.questions[idx].correctAnswer){
          correctAnswerCount++
        }
          console.log(`${this.questions.length}개의 문제 중 ${correctAnswerCount}개를 맞추셨습니다!!`)
      })
    }
 

간단하게 함수를 만들어보았습니다. 이제 정답을 체크하고 잘 작동하는지 테스트해보도록 합니다.

 

b를 선택하니 0개 , c를 선택하니 1개를 맞췄다고 잘 나오는걸 확인할 수 있습니다.

 

이제 문제를 좀 더 다뤄보도록 하겠습니다.

 

문제가 텍스트로 되어있을 수도 있지만, 이미지를 보여주고 문제를 풀어야 할 수도 있게끔 만들고 싶습니다.

 

 

mypjt의 src의 asssets에 이미지를 넣어보도록 합시다.

 

이미지의 이름은 problem + 숫자 로 만들어서 자동으로 해당 문제에 이미지가 들어갈 수 있도록 하면 좋을 것 같습니다.

 

 
<div>
      <h3 class="question-number">문제 {{index+1}}</h3>
      <p class="question-text">{{question}}</p>
      <img :src="image" alt="">
    </div>
 

문제를 보여주는 html코드를 수정합니다. img 태그를 넣어주고 src를 동적으로 받습니다.

 

src는 data에서 정해주도록 합니다.

 

mypjt 의 src폴더는 @으로 바로 접근할 수가 있지만, 단순 문자열에 @을 넣어서는 사이 불가능합니다. 

 

실제로 문자열@이 들어갈 뿐이니까요. 그렇기에 우리는 require를 사용해서 넣어주어야 합니다.

 

 

 

 
data(){
    return {
      selectedAnswer:null,
      image:require(`@/assets/problem${this.index}.png`)
    }
 

 

 

이미지가 잘 들어가는 걸 볼 수 있습니다.

 

자 이제, Submit버튼을 누른 이후의 결과를 작업해주도록 합시다.

 

일단 Submit버튼을 누르면 몇 문제를 맞췄는지, 그리고 어떤 문제를 틀렸는지와,  고른답, 실제정답이 나와야합니다.

 

그리고 기존 문제들과 Submit버튼은 사라지게 만들어야합니다.

 

일단 문제들이 사라지게 만드는 것 부터 해보도록 하겠습니다.

 

 
submitAnswers() {
      let correctAnswerCount = 0
      this.selectedAnswerList.forEach((selectedAnswer, idx)=>{
        console.log(selectedAnswer, this.questions[idx].correctAnswer)
        if(selectedAnswer === this.questions[idx].correctAnswer){
          correctAnswerCount++
        }
        this.showIndex = -1
      }
      )
          console.log(`${this.questions.length}개의 문제 중 ${correctAnswerCount}개를 맞추셨습니다!!`)
    }
 

버튼을 눌렀을 때, 작동하는 버튼입니다. 버튼을 누르면 showIndex를 -1로 설정해주면 v-show에서 걸러낼 수 있을 것 같습니다.

 

그리고 동시에 버튼을 누르면 Submit버튼도 사라지게 만들어야 하니 button에도 v-show를 넣어주도록 합니다.

 

(Router를 만들어서 아예 ResultComponent를 만들고 그곳으로 이동시키는 방법이 더욱 나을 것 같긴 한데, 

 

아직 Router를 배우지 않아 일단 이렇게 하도록 하겠습니다...

 

 
<QuestionComponent
      v-for="(question, index) in questions"
      :key="index"
      :question="question.question"
      :answer="question.answer"
      :index="index"
      v-show="index == showIndex"
      @next-move="nextMove"
      @prev-move="prevMove"
      @save-answer="saveAnswer"
    />
    <button class="submit-button" @click="submitAnswers" v-show="showIndex!=-1">Submit</button>
 

버튼에는 showIndex가 -1이 되면 사라지게 만들어주었습니다. 이제 showIndex가 -1일 때 보여줄 컴포넌트를 하나 만들도록 하겠습니다.

 

그것이 바로 ResultComponent입니다.

 

Result컴포넌트를 하나 만들고 v-show를 걸어서  showIndex가 -1일때 나오도록 해줍니다.

 

제출하기 전에는 없어도 되는 컴포넌트이니 v-if를 사용해도 될 것 같습니다. 

 

   
<ResultComponent v-if="showIndex===-1" />
 

자 이제 확인해보도록 합니다.

 

ResultComponent.vue

<template>
    <div>
        정답을 몇개 맞췄는지 보여주고

        틀린 문제들을 출력할 컴포넌트입니다.

        기존 화면이 사라지고 이 컴포넌트가 잘 나오고 있는지 테스트중입니다.
    </div>
</template>

<script>
export default {
    name: 'MypjtResultComponent',

    data() {
        return {
            
        };
    },

    mounted() {
        
    },

    methods: {
        
    },
};
</script>

<style lang="scss" scoped>

</style>

아주 완벽합니다!!! 이제 여기에 몇개의 문제를 맞췄는지 보여주기 위해서 props를 넘겨주어야 할텐데요.

 

이렇게 작업을 하고나니 submit버튼은 그저 showIndex값만 -1로 바꿔주고 그 외에 값들을 계산은 computed함수를 이용하면 더욱 깔끔해질 것 같습니다.

 

 
submitAnswers() {
     
        this.showIndex = -1
      }
  },
  computed :{
    correctAnswerCount(){
      let correctAnswerCount = 0
      this.selectedAnswerList.forEach((selectedAnswer, idx)=>{
        console.log(selectedAnswer, this.questions[idx].correctAnswer)
        if(selectedAnswer === this.questions[idx].correctAnswer){
          correctAnswerCount++
        }
      }
      )
      return correctAnswerCount
    }
  },
 

이렇게 computed를 설정해주고 이 값을 props로 넘겨주도록 합시다. 총 몇 문제인지도 체크하기 위해 그 값도 넣어줍니다.

 

 
    <ResultComponent v-if="showIndex===-1" :correct-answer-count="correctAnswerCount"/>
 

ResultComponent에서도 props로 받아줍니다.

 

 
총 {{totalCount}}의 문제 중 {{correctAnswerCount}}개의 문제를 맞췄습니다
 

이렇게 값을 추가해주면?

 

잘 나오긴 하는데... 이미지를 넣어준 이후로 이미지가 존재하지 않으면 컴포넌트가 맛이 가버리는 현상이 생겼습니다...

 

이미지가 반드시 있어야하는건 아닌데, 404에러는 생길 수 밖에 없고... 

 

img src에 바로 인덱스를 넣는 것이 아니라 부모 컴포넌트의 questions에서 이미지 주소를 미리 생성해놓고, 이미지 주소값이 넘어 왔을 때만 img태그를 만드는 작업이 필요할 것 같습니다...

 

그건 이따가 고쳐주고 일단은 이미지를 모두 채워놓도록 하겠습니다.

이렇게 제출을 하면서 느끼게 된건데, 다음문제가 없는데 다음문제 버튼이 있는것도 굉장히 불편하게 느껴집니다.

 

마지막 인덱스와 처음 인덱스에는 각각 다음 문제와 이전 문제의 버튼을 히든으로 해두어야겠습니다.

 

이 때는 v-show를 사용하면 안됩니다. 왜냐면 v-show는 태그를 사라지게 하지는 않지만 display:none으로 만들어버리거든요

 

우리는 다음문제 버튼의 위치가 바뀌는 걸 원하지 않습니다. visible을 hideen으로 바꿔주도록 클래스 바인딩을 해두록 합시다.

 
computed : {
    isFirst(){
      return this.index==0 ? true : false
    }
  },

문제는 Last일 때 다음 문제를 사라지게 하는건데요, 문제 전체의 개수를 알 수 없으니 알 수가 없습니다. 부모 컴포넌트에서 문제 개수를 넘겨주도록 합니다.

 

 
<QuestionComponent
      v-for="(question, index) in questions"
      :key="index"
      :question="question.question"
      :answer="question.answer"
      :index="index"
      :totalCount="questions.length"
      v-show="index == showIndex"
      @next-move="nextMove"
      @prev-move="prevMove"
      @save-answer="saveAnswer"
    />
 

totalCount 추가 questions.length

 

 

 
 <button @click="prevMove" class="btn next-btn" :class="{'invisible' : isFirst}"> 이전 문제</button>
 <button @click="nextMove" class="btn prev-btn" :class="{'invisible' : isLast}">다음 문제</button>
 

버튼을 추가하고 각각 computed에 정의한 함수들로 클래스를 on/off 시켜줍니다.

 

<template>
  <div>
    <div>
      <h3 class="question-number">문제 {{index+1}}</h3> 
      <p class="question-text">{{question}}</p>
      <img :src="image" alt="">
    </div>
    <div class="container d-flex justify-content-center">
      <div class="options">
        <div class="answerBox d-flex align-items-center justify-content-center ">
          <input type="radio" v-model="selectedAnswer"  :name="'answer'+index" value='a' :id="'a'+index"> <label class="ps-4 me-5" :for="'a'+index">{{ answer.a }}</label> 
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='b' :id="'b'+index"> <label class="ps-4 me-5" :for="'b'+index">{{ answer.b }} </label> 
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='c' :id="'c'+index"> <label class="ps-4 me-5" :for="'c'+index">{{ answer.c }} </label> 
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='d' :id="'d'+index"> <label class="ps-4" :for="'d'+index">{{ answer.d }} </label> 
        </div>
        <div class="d-flex justify-content-between" style="width:500px;">
          <button @click="prevMove" class="btn next-btn" :class="{'invisible' : isFirst}"> 이전 문제</button>
          <button @click="nextMove" class="btn prev-btn" :class="{'invisible' : isLast}">다음 문제</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data(){
    return {
      selectedAnswer:null,
      image:require(`@/assets/problem${this.index+1}.png`),
    }
  },
  computed : {
    isFirst(){
      return this.index==0 ? true : false
    },
    isLast(){
        return this.index==this.totalCount-1 ? true : false
    }
  },
  props:{
    question:String,
    answer:Object,
    index:Number,
    totalCount:Number,
  },
  methods:{
    nextMove(){
      this.$emit('next-move')
    },
    prevMove(){
      this.$emit('prev-move')
    },
    saveAnswer(){
      this.$emit('save-answer', {index:this.index, selectedAnswer:this.selectedAnswer})
    }
  
  },
  watch :{
    selectedAnswer :{
      handler(){
        this.saveAnswer()
      }
    }
  }
}
</script>

<style>
.question-number {
  color: #FF69B4;
  font-size: 24px;
  margin-bottom: 10px;
}

.question-text {
  color: #4B0082;
  font-size: 20px;
  margin-bottom: 20px;
  text-shadow: 2px 2px 3px #FFB6C1;
}

.options {
  background-color: #FFFACD;
  border-radius: 10px;
  box-shadow: 2px 2px 10px #D2691E;
  padding: 20px;
}

.answerBox {
  border: 1px solid black;
  width: 500px;
  height: 100px;
}

.btn {
  background-color: #FF69B4;
  border: none;
  border-radius: 10px;
  color: #FFF;
  font-size: 16px;
  font-weight: bold;
  margin-top: 20px;
  padding: 10px 20px;
  text-shadow: 2px 2px 3px #FFB6C1;
  transition: all 0.3s ease-in-out;
}

.btn:hover {
  box-shadow: 2px 2px 5px #D2691E;
  cursor: pointer;
  transform: scale(1.1);
}

.invisible{
  display:hidden,
}
</style>

 

라디오 버튼과 보기 문항 사이를 누르면 클릭이 안돼서, padding값들도 조정해주고 자잘한 버그들을 다시 수정했습니다.

 

 

자 이제 Result 컴포넌트에서 내가 틀린 문제가 무엇인지를 알려주었으면 좋겠습니다.

 

Result안에서 다시 for문을 돌려 틀린 문제들을 출력해주면 좋을 것 같습니다.

 

Result는 props로 틀린 문제 리스트를 받아야겠군요

 

틀린 문제 리스트는 어떻게 받을 수 있을까요

 

correctAnswerCount와는 반대로 맞췄을 때가 아니라 틀렸을 때를 찾으면 되고, 틀렸을 때 틀린 값을 증가시키는게 아니라 그 값들을 리스트로 반환하면 됩니다.

 

filter를 사용하면 간단하게 해결이 되겠네요

 

만들어봅시다.

 

 
computed :{
    correctAnswerCount(){
      let correctAnswerCount = 0
      this.selectedAnswerList.forEach((selectedAnswer, idx)=>{
        console.log(selectedAnswer, this.questions[idx].correctAnswer)
        if(selectedAnswer === this.questions[idx].correctAnswer){
          correctAnswerCount++
        }
      }
      )
      return correctAnswerCount
    },
      getWrongtAnswerList(){
     
      const wrongAnswerList = this.selectedAnswerList.filter((selectedAnswer, idx)=>{
        return selectedAnswer != this.questions[idx].correctAnswer
    })
    return wrongAnswerList
  },
  },
 

computed를 다음과 같이 생성해줍니다. wrongAnswerList를 잘 가져오는지 확인해보도록 합니다.

 

   
    <ResultComponent
    v-if="showIndex===-1"
    :correct-answer-count="correctAnswerCount"
    :totalCount="questions.length"
    :wrongAnswerList="getWrongtAnswerList"
    />
 

ResultComponent에 wrongAnswerList를 내려주고, 

 

 

 props :{
        correctAnswerCount:Number,
        totalCount:Number,
        wrongAnswerList:Object,
    },

ResultComponent.vue에서 받아주도록 합니다. 이제 반복문을 돌려 출력해보도록 할까요?

이런.. 틀린 답의 문제들을 내려주는게 아니라 무엇을 틀린 답으로 골랐는지를 내려주고 있습니다...

 

하지만 틀린 답도 내려줘야하는건 맞으니 일단 이렇게 남겨두고, 틀린 문제들을 어떻게 내려줄지 고민해봐야겠습니다.

문제 페이지를 만들었지만 아직 만들어야할 게 많습니다.

 

하나 하나 만들어 가도록 하겠습니다.

 

일단 첫 번째로, 라디오 버튼으로 눌러놓은 값들을 저장할 공간이 있어야 합니다.

 

라디오 버튼을 기억하고 있다고 하더라도, 그 값을 우리가 알지 못하면 정답을 눌렀는지 어쨌는지 알 수가 없기 때문이죠.

 

라디오 버튼을 눌렀을 때, 그 값을 전달해주는 방법이 뭐가 있을까요?

 

input값과 vue의 데이터가 연동이 된다? 네, v-model입니다.

 

          <input type="radio" v-model="selectedAnswer"  :name="'answer'+index" value='a' id=a> <label for="a">{{ answer.a }}</label>
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='b' id=b> <label for="b">{{ answer.b }} </label>
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='c' id=c> <label for="c">{{ answer.c }} </label>
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='d' id=d> <label for="d">{{ answer.d }} </label>

이렇게 input 버튼에 v-model을 달아주면 이 값들이 선택 될 때마다 값이 value값으로 변한다는걸 알 수 있습니다.

 

테스트를 해보니 값들이 잘 변하고 있네요

 

그럼 이제 이 받아온 값들을 언제 쏴주면 좋을까요?

 

selectedAnswer의 값이 변할 때마다 실행되는 함수가 있으면 참 좋을 것 같습니다.

 

이 selectedAnswer값을 보고 있다가 값이 변하면 실행하는 함수!! watch 속성을 사용하면 어떨까요?

 

watch 속성을 추가해주도록 하겠습니다.

 

  watch :{
    selectedAnswer :{
      handler(){
        console.log(this.selectedAnswer)
      }
    }
  }

watch 속성을 추가하고 selectedAnswer를 주목하도록 합니다.

 

그리고 이 값이 변할 때 handler(){} 함수가 작동을 하게 됩니다.

 

여기서 함수를 직접 정의하기보다는 methods에 함수를 정의해주고 가져다 사용하는 것이 더욱 나은 방법입니다.

 

watch 속성을 주니 값이 잘 들어가는지 테스트 하는 버튼을 누르지 않아도 자동으로 들어가는 걸 확인 할 수가 있네요.

 

자 그럼 이제 이 값을 고른 정답 리스트에 담는 작업을 하도록 합시다.

 

methods를 하나 만들어줄건데 이건 부모컴포넌트의 고른 정답 리스트에 값을 담아주는 역할을 할겁니다.

 

부모 컴포넌트에 값을 전달해주려면 다시 emit을 사용하면 됩니다.

 

    nextMove(){
      this.$emit('next-move')
    },
    prevMove(){
      this.$emit('prev-move')
    },
    saveAnswer(){
      this.$emit('save-answer')
    }
 
  },
  watch :{
    selectedAnswer :{
      handler(){
        this.saveAnswer()
      }
    }
  }

이렇게 methods에 함수를 설정해주고 handler안에서 사용해줍니다.

 

그 다음은 부모 컴포넌트에서 save-answer라는 이벤트로 인덱스와 고른 값을 받은 뒤, 그 값을 이용해 selectedAnswerList에 넣어주면 됩니다.

saveAnswer(){
      this.$emit('save-answer', {index:this.index, selectedAnswer:this.selectedAnswer})
    }
 

saveAnswer를 다음과 같이 수정해준뒤, 부모 컴포넌트에서 받아주도록 합니다.

 

부모 컴포넌트에 selectedAnswerList를 만들고 함수도 실행시켜주고 하면

 

<template>
  <div id="app" class="container fw-bold">
    <QuestionComponent
      v-for="(i, index) in questionAmount"
      :key="index"
      question="세상에서 가장 귀여운 동물은?"
      :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
      :index="index"
      v-show="index == showIndex"
      @next-move="nextMove"
      @prev-move="prevMove"
      @save-answer="saveAnswer"
    />
    <button class="submit-button" @click="submitAnswers">Submit</button>
  </div>
</template>

<script>
import _ from 'lodash'
import QuestionComponent from './components/QuestionComponent.vue';

export default {
  name: 'App',
  data(){
    return {
      questionAmount : _.range(30),
      showIndex : 0,
      selectedAnswerList : new Array(30).fill(''),
      
    }
  },
  
  components:{
    QuestionComponent
  },
  methods: {
    nextMove(){
      this.showIndex = this.showIndex+1 > 29 ? 29 : this.showIndex+1
    },
    prevMove(){
      this.showIndex = this.showIndex - 1 < 0 ? 0 : this.showIndex-1
    },
    saveAnswer({index, selectedAnswer}){
      this.selectedAnswerList[index] = selectedAnswer
      console.log(this.selectedAnswerList)
    },
    submitAnswers() {
      // Your logic for submitting answers here
    }
  
  }
  }

</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  margin-top: 60px;
}

.container {
  background-color: #fce4ec;
  border-radius: 10px;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
  padding: 40px;
}

.question-text {
  font-size: 24px;
  color: #333333;
  margin-bottom: 20px;
}

.answer-label {
  font-size: 20px;
  color: #333333;
  margin-bottom: 10px;
}

.answer-input {
  margin-right: 10px;
}

.submit-button {
  background-color: #ff69b4;
  border: none;
  border-radius: 50px;
  color: white;
  font-size: 18px;
  padding: 10px 30px;
  margin-top: 30px;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
  transition: all 0.3s ease-in-out;
}

.submit-button:hover {
  background-color: #ff94c2;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.8);
  cursor: pointer;
}
</style>

 

값을 선택할 때 마다 해당 위치에 선택한 값들이 잘 들어가는 것을 확인할 수 있습니다.

 

이제 correctAnswerList를 하나 만들어서 대조시켜주면 내가 무슨 문제를 틀렸는지도 바로 알 수 있겠네요!!!

 

값들이 차곡차곡 잘 쌓이는 것을 알 수 있습니다!!

 

이제 Submit버튼을 누르면 correctAnswerList와 값을 비교하고 틀린 문제들을 체크해주면 될 것 같습니다.

 

자 이제 여기까지 왔으니 슬슬 진짜 문제들을 만들어 보도록 합니다.

 

지금은 문제가 바껴도 매번 같은 문제들만 나오고 있는데, 문제를 옮기면 당연히 문제도 달라지고 답도 달라져야하죠

 

이제 문제들을 각각 Object로 바꿔주도록 하겠습니다.

 

 <QuestionComponent
      v-for="(i, index) in questionAmount"
      :key="index"
      question="세상에서 가장 귀여운 동물은?"
      :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
      :index="index"
      v-show="index == showIndex"
      @next-move="nextMove"
      @prev-move="prevMove"
      @save-answer="saveAnswer"
    />

여기서 questionAmount로 그냥 문제 개수만큼 반복문을 돌리는 것이 아니라 문제 Object를 순회하면서

 

question과 answer에 각각 다른 값들을 쏴주겠다 이말입니다

 

그럼 일단 questionAmount를 questions로 바꾸어야 하겠습니다.

 

questions로 하나 하나 만들려고 하다 보니 갑자기 30개라는 문제양이 너무 많아보입니다.

 

5개로 줄이도록 하겠습니다.

 

data(){
    return {
      questionAmount : _.range(30),
      showIndex : 0,
      selectedAnswerList : new Array(30).fill(''),     
    }
  },

data부분을 수정해주도록 하겠습니다.

 

 data(){
    return {
      questions :[{
        question : '다음 중 세상에서 가장 귀여운 동물은?',
        answer : {
          a:'강아지',
          b:'고양이',
          c:'김나리',
          d:'토끼',
        },
      }],
      showIndex : 0,
      selectedAnswerList : new Array(30).fill(''),
     
     
    }
  },

 

이런식으로 말이죠. 그리고 컴포넌트에 쏴줄 props도 바꿔주도록 합니다.

 

 <QuestionComponent
      v-for="(question, index) in questions"
      :key="index"
      :question="question.question"
      :answer="question.answer"
      :index="index"
      v-show="index == showIndex"
      @next-move="nextMove"
      @prev-move="prevMove"
      @save-answer="saveAnswer"
    />

이렇게 QuestionComponent도 바꿔줍니다. questionAmount를 도는게 아니라 questions를 돌고

 

question과 answer는 이제 question 안에 있는 question과 answer를 전달해줄겁니다.

question도 이제 dynmaic props로 바꿔주어야겠죠.

 

문제 5개를 만들어 놓고 테스트를 해보도록 합니다.

questions :[{
        question : '다음 중 세상에서 가장 귀여운 동물은?',
        answer : {
          a:'강아지',
          b:'고양이',
          c:'김나리',
          d:'토끼',
        },
      },
      {
        question : '다음 중 가장 맛있는 음료수는??',
        answer : {
          a:'펩시',
          b:'코카콜라',
          c:'웰치스',
          d:'환타',
        },
      },

이런식으로 하나 하나 문제들을 수정해주도록 합니다.

 

매우 만족스러운 결과가 나왔습니다.

 

이제 정말 문제를 하나하나 풀어볼 수 있는 페이지가 완성이 되었습니다.

 

이왕 문제 오브젝트를 만든 김에 각 오브젝트들이 correctAnswer도 가지고 있게 해줍시다.

 questions :[{
        question : '다음 중 세상에서 가장 귀여운 동물은?',
        answer : {
          a:'강아지',
          b:'고양이',
          c:'김나리',
          d:'토끼',
        },
        correctAnswer : 'c'
      },
      {
        question : '다음 중 가장 맛있는 음료수는??',
        answer : {
          a:'펩시',
          b:'코카콜라',
          c:'웰치스',
          d:'환타',
          correctAnswer : 'a'
        },
      },
      {
        question : '다음 중 가장 재미있는 프로그래밍 언어는?',
        answer : {
          a:'파이썬',
          b:'C',
          c:'Java',
          d:'HTML',
        },
        correctAnswer : 'd'
      },

이런식으로 각자가 correctAnswer까지 가지고 있으면 이후에 정답을 체크하기에 좀 더 수월해질 수 있을 것 같습니다.

selectedAnswerList도 이제 굳이 Array로 여러개 만들지 않고 문제의 개수만큼만 만들어주면 되겠죠

 

next버튼과 prev버튼을 누를 때 문제 길이 이상으로 넘어가지 못하게 하는 것도 수정해주고 말이죠.

 

추가로 문제가 바꼈을 때 문제별 정답버튼의 id를 고정시켜버려서 

 

다음 문제부터는 label을 눌러도 라디오 버튼이 클릭이 되지 않기에,  :id와 :for으로 동적으로 받아주어 문제를 해결했습니다.

<template>
  <div id="app" class="container fw-bold">
    <QuestionComponent
      v-for="(question, index) in questions"
      :key="index"
      :question="question.question"
      :answer="question.answer"
      :index="index"
      v-show="index == showIndex"
      @next-move="nextMove"
      @prev-move="prevMove"
      @save-answer="saveAnswer"
    />
    <button class="submit-button" @click="submitAnswers">Submit</button>
  </div>
</template>

<script>
import QuestionComponent from './components/QuestionComponent.vue';

export default {
  name: 'App',
  data(){
    return {
      questions :[{
        question : '다음 중 세상에서 가장 귀여운 동물은?',
        answer : {
          a:'강아지',
          b:'고양이',
          c:'김나리',
          d:'토끼',
        },
        correctAnswer : 'c'
      },
      {
        question : '다음 중 가장 맛있는 음료수는??',
        answer : {
          a:'펩시',
          b:'코카콜라',
          c:'웰치스',
          d:'환타',
        },
          correctAnswer : 'a'
      },
      {
        question : '다음 중 가장 재미있는 프로그래밍 언어는?',
        answer : {
          a:'파이썬',
          b:'C',
          c:'Java',
          d:'HTML',
        },
        correctAnswer : 'd'
      },
      {
        question : '다음 중 가장 좋은 계절은?',
        answer : {
          a:'봄',
          b:'여름',
          c:'가을',
          d:'겨울',
        },
        correctAnswer:'b'
      },
      {
        question : '다음 중 가장 맛있는 야식은?',
        answer : {
          a:'치킨',
          b:'피자',
          c:'족발',
          d:'햄버거',
        },
          correctAnswer:'a'
      },
      ],
      showIndex : 0,
      selectedAnswerList : [],
     
      
    }
  },
  
  
  components:{
    QuestionComponent
  },
  methods: {
    nextMove(){
      this.showIndex = this.showIndex+1 > this.questions.length -1 ? this.questions.length -1 : this.showIndex+1
    },
    prevMove(){
      this.showIndex = this.showIndex - 1 < 0 ? 0 : this.showIndex-1
    },
    saveAnswer({index, selectedAnswer}){
      this.selectedAnswerList[index] = selectedAnswer
      console.log(this.selectedAnswerList)
    },
    submitAnswers() {
      // Your logic for submitting answers here
    }
  },
  created(){
    this.selectedAnswerList = new Array(this.questions.length).fill('')
  }
  }

</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  margin-top: 60px;
}

.container {
  background-color: #fce4ec;
  border-radius: 10px;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
  padding: 40px;
}

.question-text {
  font-size: 24px;
  color: #333333;
  margin-bottom: 20px;
}

.answer-label {
  font-size: 20px;
  color: #333333;
  margin-bottom: 10px;
}

.answer-input {
  margin-right: 10px;
}

.submit-button {
  background-color: #ff69b4;
  border: none;
  border-radius: 50px;
  color: white;
  font-size: 18px;
  padding: 10px 30px;
  margin-top: 30px;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
  transition: all 0.3s ease-in-out;
}

.submit-button:hover {
  background-color: #ff94c2;
  box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.8);
  cursor: pointer;
}
</style>
<template>
  <div>
    <div>
      <h3 class="question-number">문제 {{index+1}}</h3> 
      <p class="question-text">{{question}}</p>
    </div>
    <div class="container d-flex justify-content-center">
      <div class="options">
        <div class="answerBox d-flex align-items-center justify-content-evenly">
          <input type="radio" v-model="selectedAnswer"  :name="'answer'+index" value='a' :id="'a'+index"> <label :for="'a'+index">{{ answer.a }}</label> 
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='b' :id="'b'+index"> <label :for="'b'+index">{{ answer.b }} </label> 
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='c' :id="'c'+index"> <label :for="'c'+index">{{ answer.c }} </label> 
          <input type="radio" v-model="selectedAnswer" :name="'answer'+index" value='d' :id="'d'+index"> <label :for="'d'+index">{{ answer.d }} </label> 
        </div>
        <div class="d-flex justify-content-between" style="width:500px;">
          <button @click="prevMove" class="btn next-btn">이전 문제</button>
          <button @click="nextMove" class="btn prev-btn">다음 문제</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data(){
    return {
      selectedAnswer:null,
    }
  },
  props:{
    question:String,
    answer:Object,
    index:Number,
  },
  methods:{
    nextMove(){
      this.$emit('next-move')
    },
    prevMove(){
      this.$emit('prev-move')
    },
    saveAnswer(){
      this.$emit('save-answer', {index:this.index, selectedAnswer:this.selectedAnswer})
    }
  
  },
  watch :{
    selectedAnswer :{
      handler(){
        this.saveAnswer()
      }
    }
  }
}
</script>

<style>
.question-number {
  color: #FF69B4;
  font-size: 24px;
  margin-bottom: 10px;
}

.question-text {
  color: #4B0082;
  font-size: 20px;
  margin-bottom: 20px;
  text-shadow: 2px 2px 3px #FFB6C1;
}

.options {
  background-color: #FFFACD;
  border-radius: 10px;
  box-shadow: 2px 2px 10px #D2691E;
  padding: 20px;
}

.answerBox {
  border: 1px solid black;
  width: 500px;
  height: 100px;
}

.btn {
  background-color: #FF69B4;
  border: none;
  border-radius: 10px;
  color: #FFF;
  font-size: 16px;
  font-weight: bold;
  margin-top: 20px;
  padding: 10px 20px;
  text-shadow: 2px 2px 3px #FFB6C1;
  transition: all 0.3s ease-in-out;
}

.btn:hover {
  box-shadow: 2px 2px 5px #D2691E;
  cursor: pointer;
  transform: scale(1.1);
}
</style>

자 다음에는 이제 submit버튼을 눌러 맞춘 정답이 몇개나 되는지를 만들어보도록 하겠습니다

나만의 작고 귀여운 사이트가 하나 만들어졌습니다.

 

하지만 문제가 1개밖에 없다는 게 너무 아쉽습니다

 

문제를 30개 정도는 만들어 줘야 문제풀이 사이트라고 할 수 있을텐데 말이죠.

 

문제를 30개 만들어보도록 하겠습니다.

 

<template>
  <div id="app" class="fw-bold">
    <QuestionComponent 
    question="세상에서 가장 귀여운 동물은?"
    :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
    />
  </div>
</template>

 

우리가 이럴려고 문제와 정답을 App.vue에서 QuestionComponent에 뿌려준거 아니겠습니까

 

v-for 을 사용하여 30개의 문제를 아주 간단하게 만들 수 있습니다.

 

<script>
import _ from 'lodash'
import QuestionComponent from './components/QuestionComponent.vue';

export default {
  name: 'App',
  data(){
    return {
      questionAmount : _.range(30)
    }
  },
  components:{
    QuestionComponent
  }

}
</script>

이렇게 data안에 길이 30의 리스트를 만들고 그 리스트를 for 돌려주면 30개가 만들어질테니까 말이죠.

 

오 문제 30개가 생겼습니다. 하지만 문제가 전부 1이라고 나오는게 굉장히 거슬리네요. 

 

문제의 인덱스도 props로 넘겨줘서 문제별 숫자를 다르게 만들어주도록 하겠습니다.

<template>
  <div id="app" class="fw-bold">
    <QuestionComponent v-for="(i, index) in questionAmount" :key="index"
    question="세상에서 가장 귀여운 동물은?"
    :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
    :index="index"
    />
  </div>
</template>

 

for문을 돌릴 때, index값도 같이 받아준 뒤 index값을 props로 넘겨주도록 합니다.

 

 props:{
    question:String,
    answer:Object,
    index:Number,
  }
<template>
  <div>
    <div>
    <h3>문제 {{index+1}}</h3> 
    <p>{{question}}</p>
    </div>
    <div class="container d-flex justify-content-center">

    <div> &lt;보기&gt;
    <div class="answerBox d-flex align-items-center justify-content-evenly">
      <input type="radio" name="answer" value='a' id=a> <label for="a">{{ answer.a }}</label> 
      <input type="radio" name="answer" value='b' id=b> <label for="b">{{ answer.b }} </label> 
      <input type="radio" name="answer" value='c' id=c> <label for="c">{{ answer.c }} </label> 
      <input type="radio" name="answer" value='d' id=d> <label for="d">{{ answer.d }} </label> 

    </div>
    </div>
    </div>
  </div>
</template>

1번부터 차근차근 잘 나오는걸 확인할 수 있습니다.

 

근데 이렇게 문제를 한 페이지에 모두 다 보여주는게 또 너무 아쉽습니다...

 

뭔가 버튼을 눌러서 한 문제씩 보여줄 수 있다면 참 좋을것 같은데 말이죠...

 

우리가 index를 만들어냈으니 index를 활용해보면 참 좋을 것 같습니다.

 

인덱스 값이 1인것만 보여줘, 2인것만 보여줘 이런식으로 하면 필요한 문제만 볼 수 있을테니까 말이죠!!

 

여기에 v-if까지 넣어서 수정을 해주면 참 좋을것 같지만, Vue는 v-for과 v-if를 같이 사용하면 이렇게 에러가 터져버립니다.

 

 

대충 v-if랑 v-for 같이 쓰지 말라는 내용인데요...

 

우리는 v-if를 사용하면 그 페이지를 제외한 다른 것들을 사라지게 되니, if문을 쓸거면 애초에 그냥 computed를 쓰던 뭘 쓰던 해서 필터링을 먼저 한 뒤 v-for문 안에 필터링 된 오브젝트를 써라 뭐 할라고 굳이 v-for문으로 다 뿌려놓고 v-if문으로 걸르냐 라는 소리입니다.

 

하지만 우리는 문제를 필터링 시키면 안됩니다. 문제를 무조건 다 for문으로 가져오긴 해야하는데 보고 싶은것만 보고 싶다 이겁니다!!

 

그럴 때 사용할 수 있는게 v-show입니다. v-if 와 비슷한 작용을 하지만 html태그가 사라지지 않고 남아있으면서 display만 none으로 바꿔주는 녀석입니다.

 

그렇기에 v-for문과 같이 써도 에러를 터뜨리지 않고 잘 작동해줍니다. 기특한 녀석

 

자 이제 v-if를 없애버리고 v-show를 사용해줍니다.

 

컴포넌트에 이렇게 추가를 해봅니다. index가 1번이니 문제 2번 하나만 나와야겠죠?

  <QuestionComponent v-for="(i, index) in questionAmount" :key="index"
    question="세상에서 가장 귀여운 동물은?"
    :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
    :index="index"
    v-show="index==1"
    />

 

완벽합니다 

 

자 이제 문제1을 보고 싶은데요...

 

....v-show를 고정시켜버렸으니 우리는 문제1을 볼 수가 없습니다.

 

v-show 안에 변수를 넣어서 index === (변수) 로 설정해주고 버튼을 누를때마다 변수 값을 바꿔서 보여주는 인덱스를 컨트롤 하면 참 좋을 것 같습니다.

 

바로 해보도록 합시다.

 

    <QuestionComponent v-for="(i, index) in questionAmount" :key="index"
    question="세상에서 가장 귀여운 동물은?"
    :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
    :index="index"
    v-show="index==showIndex"
    />

 

자 이제 우리는 이 showIndex를 컨트롤할 수 있는 버튼을 만들어 주어야 합니다.

 

App.vue에 만들지 QuestionComponent에 만들지는 자유입니다.

 

QuestionComponent에 만드는게 더 맞는거 같다는 생각이 들긴 하지만... App.vue에 만들어도 딱히 상관은 없을 것 같습니다.

 

App.vue에 넣어보도록 합니다.

 

어차피 showIndex만 컨트롤해주면 되는거라...딱히 상관은 없지만... 문제 박스 안에 보기 밑에 딱 버튼을 고것의 밑으로 딱 배치하고 싶은디 버튼을 자식컴포넌트 안의 div안으로 넣지를 못하니 이거... 참 귀찮아집니다.

 

? 그럼 저 QuestionComponent와 버튼을 하나의 div로 묶으면 되는거 아니냐! 라고 하실 수도 있습니다.

 

머리를 잘 굴리면 굳이 안 넣고도 배치를 잘 맞출 수 있을 것 같지만 저는 굳이 자식 컴포넌트의 보기 아래에 버튼을 배치하고 싶습니다.

 

사실 QuestionComponent에 버튼을 넣고 emit으로 넘겨주는거 하고 싶어 빌드업 한거였습니다.

 

그냥 QuestionComponent에 넣어주도록 합니다.

 

<template>
  <div>
    <div>
    <h3>문제 {{index+1}}</h3> 
    <p>{{question}}</p>
    </div>
    <div class="container d-flex justify-content-center">

    <div> &lt;보기&gt;
    <div class="answerBox d-flex align-items-center justify-content-evenly">
      <input type="radio" name="answer" value='a' id=a> <label for="a">{{ answer.a }}</label> 
      <input type="radio" name="answer" value='b' id=b> <label for="b">{{ answer.b }} </label> 
      <input type="radio" name="answer" value='c' id=c> <label for="c">{{ answer.c }} </label> 
      <input type="radio" name="answer" value='d' id=d> <label for="d">{{ answer.d }} </label> 

    </div>
    <div class="d-flex justify-content-between" style="width:500px;">

    <button>다음 문제</button>
    <button>이전 문제</button>
    </div>
    </div>
    </div>
  </div>
</template>

<script>
export default {
  props:{
    question:String,
    answer:Object,
    index:Number,
  }
}
</script>

<style>
.answerBox{
  border: 1px solid black;
  width: 500px;
  height: 100px;
}
</style>

이거 보세요 캬... 이럴려고 Component별로 나누는거 아니겠습니까

 

같은 컴포넌트 안에 넣어주자마자 바로 레이아웃이 완벽하게 딱 들어맞습니다

 

다음 문제와 이전 문제의 버튼 위치가 왜 이런다냐...

 

하여튼 이제 우리는 버튼들을 잘 배치했으니 문제 버튼을 누를 때마다 문제를 샥 샥 이동시켜주면 되는겁니다.

 

자 그럼 자식 컴포넌트에서 버튼을 눌러 부모 컴포넌트의 showIndex를 조작해주면 되겠군요!!

 

일단 CSS를 살짝 넣고 다시 돌아오겠습니다.

 

각자 CSS를 넣어보도록 합니다.

 

CSS로 이쁘게 만드는건 너무 어려운 작업입니다.

 

ChatGPT한테 해달라고 합시다.

 

자 CSS를 넣고 왔습니다 

 

개인적인 취향입니다. 버튼을 누르면 shadow넣고 transform:scale 커지게 만드는건 제가 정말 애용하는 기능 중 하나입니다.

 

submit버튼도 지멋대로 만들어놓고, hover에 pink-shadow까지 넣어줬네요 아주.. 맘에 듭니다.

 

transition ease-in-out도 걸어서 애니메이션 효과까지 넣어줬네요

 

뭐.. CSS기능들 이해 하고 나중에 누가 만들어달라고 하면 만들 수만 있으면 되는거 아니겠습니까...

 

다 할 줄은 아는 기능인데 내가 하면 안 이뻐서 그렇지....

 

갓GPT입니다 하여튼...

 

 

 

자 그럼 이제 다음 문제를 눌렀을 때 문제의 번호가 바뀌도록 해줍니다.

 

다음 문제 버튼을 클릭하면 emit 이벤트가 발생하도록 해야합니다.

 <button @click="prevMove" class="btn next-btn">이전 문제</button>
 <button @click="nextMove" class="btn prev-btn">다음 문제</button>

각 버튼에 methods를 넣어준 뒤,

 

  methods:{
    nextMove(){
      this.$emit('next-move')
    },
    prevMove(){
      this.$emit('prev-move')
    }
  
  }

각 메소드에서는 emit 이벤트를 발생시키도록 합니다. 

 

   <QuestionComponent
      v-for="(i, index) in questionAmount"
      :key="index"
      question="세상에서 가장 귀여운 동물은?"
      :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
      :index="index"
      v-show="index == showIndex"
      @next-move="nextMove"
      @prev-move="prevMove"
    />
 
 

와우... QuestionComponent에 속성들이 계속해서 늘어나고 있습니다... v-for , v-show, static-props, dynamic-props, 거기에 emit이벤트까지 다 쓰여지고 있네요

 

자 그럼 이제 emit이벤트가 발생했을대 실행시킬 method까지 정의를 해주도록 합니다.

 

 methods: {
    nextMove(){
      this.showIndex = this.showIndex+1 > 29 ? 29 : this.showIndex+1
    },
    prevMove(){
      this.showIndex = this.showIndex - 1 < 0 ? 0 : this.showIndex-1
    },

29 넘어가면 29로 바꿔주고, 0보다 작아지면 0으로 바꿔주는 그런 로직을 추가했습니다. 

 

if문으로 처리하셔도 됩니다.

완벽합니다

 

근데 문제가 하나 더 발생하고 있습니다.

 

문제를 넘기고나서 라디오버튼을 누르면 이전 라디오 버튼이 사라지는 문제입니다.

사라진 라디오 버튼

 

같은 name을 공유하고 있어서 그런 것 같습니다.

 

name값에도 해당 인덱스 값을 추가로 넣어주도록 합니다. 

 

name값에 변수를 넣어주어야 하니 name을 v-bind시켜주도록 하구요

 

<input type="radio" :name="'answer'+index" value='a' id=a> <label for="a">{{ answer.a }}</label> 
<input type="radio" :name="'answer'+index" value='b' id=b> <label for="b">{{ answer.b }} </label> 
<input type="radio" :name="'answer'+index" value='c' id=c> <label for="c">{{ answer.c }} </label> 
<input type="radio" :name="'answer'+index" value='d' id=d> <label for="d">{{ answer.d }} </label>

대충 이렇게 input에 name을 바인딩 시켜주도록 하자 이겁니다.

 

그럼 각 문제별 다른 라디오 버튼이 나올테니까 말이죠

이제 다른 문제를 가서 다른 답변을 선택하더라도

기존의 문제는 변하지 않은 것을 알 수 있습니다!!

 

이제 문제를 만들 준비가 점차 되어가고 있는 것 같습니다.

 

 

Vue 를 배운지 벌써 1주일이 지나가는 것 같은데 뭐라도 안 만들어보면 금방 또 까먹게 될 것 같아 

 

웹 페이지 하나를 만들어보려고 합니다.

 

node는 웹페이지에서 다운로드 받으시고ㅡ,

 

npm install -g @vue/cli 를 이용하시면 vue/cli를 다운로드 받을 수 있습니다.

 

node.js와 vue/cli를 잘 깔아 뒀으니 이제 mypjt 뷰 프로젝트를 하나 만들겠습니다.\

 

vue create mypjt 로 뷰 프로젝틀르 만들어줍니다.

 

cd mypjt -> npm run serve 로 

 

뷰 프로젝트가 잘 완성된 것을 확인할 수 있습니다.

 

이제 쓸데없는 HelloWorld.vue 컴포넌트를 없애준뒤 시작해보겠습니다.

 

자 HelloWorld 대신 QustionComponent를 만들고 components에도 잘 추가해주었습니다.

 

자 이제, App.vue에서 Question Component에 문제들을 전달해 줄겁니다.

 

여러개의 Question Component를 App vue에 만들건데 각자 다른 정보들을 가지고 있어야 합니다.

 

Question Component안에 문제를 만들어 버리면 거기서 컨트롤을 하기가 좀 어려울 것 같아 App.vue에서 props로 전달해 주도록 하겠습니다.

 

일단 잘 작동을 하는지 테스트용으로 question과 answer을 넘겨보도록 하겠습니다.

 

<template>
  <div id="app">
    <QuestionComponent 
    question="세상에서 가장 귀여운 동물은?"
    :answer="{a:'강아지', b:'고양이', c:'토끼', d:'페코'}"
    />
  </div>
</template>

 

자 QuestionComponent에서 값을 잘 받을 수 있도록 QuestionComponent의 props에서 정의를 해주도록 합시다.

 

 

<script>
export default {
  props:{

    question:String,
    answer:Object,
  }
}
</script>

props에서 받아주어야 한다는걸 까먹으면 안되겠습니다.

 

그리고 html에 적절하게 넣어서 출력을 해보도록 하겠습니다.

<template>
  <div>
    <div>
    <h3>문제 1</h3>
    <p>{{question}}</p>
    </div>
    <div>
    <p> &lt;보기&gt;</p>
    <p>{{answer}}</p>
    </div>
  </div>
</template>

아주 좋습니다. 이제 이것들을 가공해서 이뻐보이게 만들면 되겠군요

 

좀 이뻐보이게 만들기 위해서 bootstrap을 깝니다. npm install bootstrap 해주면 됩니다.

 

그런 뒤 main.js에서 

import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'

이 2개를 css에 추가해주면 됩니다.

 

보기가 훨씬 나아졌습니다.

 

오브젝트 타입에 있는 아이들을 하나씩 꺼내주고 라디오 버튼을 달아놓았습니다

 

 

<template>
  <div>
    <div>
    <h3>문제 1</h3> 
    <p>{{question}}</p>
    </div>
    <div class="container d-flex justify-content-center">

    <div> &lt;보기&gt;
    <div class="answerBox d-flex align-items-center justify-content-evenly">
      <input type="radio" name="answer" value='a' id=a> <label for="a">{{ answer.a }}</label> 
      <input type="radio" name="answer" value='b' id=b> <label for="b">{{ answer.b }} </label> 
      <input type="radio" name="answer" value='c' id=c> <label for="c">{{ answer.c }} </label> 
      <input type="radio" name="answer" value='d' id=d> <label for="d">{{ answer.d }} </label> 

    </div>
    </div>
    </div>
  </div>
</template>

<script>
export default {
  props:{
    question:String,
    answer:Object,
  }
}
</script>

<style>
.answerBox{
  border: 1px solid black;
  width: 500px;
  height: 100px;
}
</style>

 

 

+ Recent posts