isTrue 속성을 추가하여 정답/오답을 구분할 수 있도록 하였다.퀴즈 풀고 정답/오답 기록하기
풀었던 퀴즈는 나오지 않게 하기
isSkiped 속성을 추가하였고 skip이면 true, skip이 아니면 false로 속성값을 부여했다. → 이 속성을 의존성 배열에 추가했더니 타이머가 정상적으로 동작했다.
import logoImg from "../assets/quiz-logo.png";
export default function Header() {
return (
<header>
<img src={logoImg} alt="Quiz logo" />
<h1>ReactQuiz</h1>
</header>
);
}
import { useState } from "react";
import QUESTIONS from "../questions.js";
export default function Quiz() {
const [userAnswers, setUserAnswers] = useState([]); // 답 등록
const activeQuestionIndex = userAnswers.length;
function handleSelectAnswer(selectedAnswer) {
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
}
return (
<div id="quiz">
<div id="question">
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
<ul id="answers">
{QUESTIONS[activeQuestionIndex].answers.map((answer) => (
<li key={answer} className="answer">
<button onClick={() => handleSelectAnswer(answer)}>
{answer}
</button>
</li>
))}
</ul>
</div>
</div>
);
}
answers 배열안의 첫번째 text가 문제에 대한 답이고 이를 userAnswers 상태에 저장/업데이트 할 것이다.userAnswers의 길이가 곧 현재 활성화 된 질문의 인덱스번호이다. → 초기에는 이 배열의 길이는 0이므로 0번째 인덱스에 접근하게 되며 0번째 인덱스의 질문이 첫번째 질문이 된다.import Header from "./components/Header";
import Quiz from "./components/Quiz";
function App() {
return (
<>
<Header />
<main>
<Quiz />
</main>
</>
);
}
export default App;

import { useState } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
export default function Quiz() {
const [userAnswers, setUserAnswers] = useState([]); // 답 등록
const activeQuestionIndex = userAnswers.length;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length; // 존재하고있는 질문 양과 인덱스값이 같으면 true 반환
function handleSelectAnswer(selectedAnswer) {
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
}
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
// 위의 quizIsComplete와 관련된 로직 아래에 위치해야한다. 해당 부분을 먼저 검사 후 셔플을 진행 -> 화면에 렌더링하는 순서여야 함.
// 즉 다음의 것들은 남은 질문들이 있을 때 수행되는 것이다.
const shuffledAnswers = [...QUESTIONS[activeQuestionIndex].answers];
shuffledAnswers.sort(() => Math.random() - 0.5);
return (
<div id="quiz">
<div id="question">
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
<ul id="answers">
{shuffledAnswers.map((answer) => (
<li key={answer} className="answer">
<button onClick={() => handleSelectAnswer(answer)}>
{answer}
</button>
</li>
))}
</ul>
</div>
</div>
);
}
shuffledAnswers.sort(()=> Math.random() - 0.5 )sort() : 두 개의 요소(매개변수)가 필요하고 만약 음수를 반환하면, 해당 요소들의 위치가 바뀐다. 이에 비해 양수를 반환하면, 원래 순서를 유지한다.() => Math.random() - 0.5 ==> 반은 양수, 반은 음수로 하여 셔플할 것이다.
import { useState, useEffect } from "react";
export default function QuestionTimer({ timeout, onTimeout }) {
const [remainingTime, setRemainingTime] = useState(timeout);
useEffect(() => {
// onTimeout : 부모에게 넘겨서 해당 문제를 못 풀었음을 알려야함.
setTimeout(onTimeout, timeout); // onTimeout, timeout 속성을 사용함. => 의존성이 변경되면 재실행
}, [onTimeout, timeout]);
// 부모 컴포넌트가 QuestionTimer의 timeout이 변경되어야 하는 지 결정하기 때문에 타이머를 초기화하고 다시 실행할 필요가 있다.
useEffect(() => {
setInterval(() => {
setRemainingTime((prevRemainingTime) => prevRemainingTime - 100);
}, 100);
}, []);
return <progress id="question-time" value={remainingTime} max={timeout} />;
}
import { useState } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
import QuestionTimer from "./QuestionTimer.jsx";
export default function Quiz() {
const [userAnswers, setUserAnswers] = useState([]);
const activeQuestionIndex = userAnswers.length;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
function handleSelectAnswer(selectedAnswer) {
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
}
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
const shuffledAnswers = [...QUESTIONS[activeQuestionIndex].answers];
shuffledAnswers.sort(() => Math.random() - 0.5);
return (
<div id="quiz">
<div id="question">
{/* handleSelectAnswer(null)로 설정함으로써 해당 질문에 어떠한 답변하지 않고 넘어갔음을 상태에 알림 */}
<QuestionTimer
timeout={10000}
onTimeout={() => handleSelectAnswer(null)}
/>
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
<ul id="answers">
{shuffledAnswers.map((answer) => (
<li key={answer} className="answer">
<button onClick={() => handleSelectAnswer(answer)}>
{answer}
</button>
</li>
))}
</ul>
</div>
</div>
);
}

import { useState, useEffect } from "react";
export default function QuestionTimer({ timeout, onTimeout }) {
const [remainingTime, setRemainingTime] = useState(timeout);
useEffect(() => {
console.log("SETTING TIMEOUT");
setTimeout(onTimeout, timeout);
}, [onTimeout, timeout]);
useEffect(() => {
console.log("SETTING INTERVAL");
setInterval(() => {
setRemainingTime((prevRemainingTime) => prevRemainingTime - 100);
}, 100);
}, []);
return <progress id="question-time" value={remainingTime} max={timeout} />;
}

onTimeout={()=> handleSelectAnswer(null)}이 재실행된다.import { useState, useCallback } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
import QuestionTimer from "./QuestionTimer.jsx";
export default function Quiz() {
const [userAnswers, setUserAnswers] = useState([]);
const activeQuestionIndex = userAnswers.length;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
const handleSelectAnswer = useCallback(function handleSelectAnswer(
selectedAnswer
) {
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
},
[]); // 여기엔 추가하지 않아도 됨.
// handleSelectAnswer 함수에서 상태나 속성 그리고 이에 의존하는 다른 어떠한 값도 사용하고 있지 않다.
// 상태를 업데이트하는 함수(setUserAnswers)는 추가될 필요 없다. -> 리액트가 그들이 절대 바뀌지 않도록 보장하기 때문이다.
const handleSkipAnswer = useCallback(() => {
handleSelectAnswer(null); // handleSelectAnswer 의존성을 사용함. => 해당 컴포넌트 함수에서 생성된 된 값이니까!
}, [handleSelectAnswer]);
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
const shuffledAnswers = [...QUESTIONS[activeQuestionIndex].answers];
shuffledAnswers.sort(() => Math.random() - 0.5);
return (
<div id="quiz">
<div id="question">
{/* handleSelectAnswer(null)로 설정함으로써 해당 질문에 어떠한 답변하지 않고 넘어갔음을 상태에 알림 */}
<QuestionTimer timeout={10000} onTimeout={handleSkipAnswer} />
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
<ul id="answers">
{shuffledAnswers.map((answer) => (
<li key={answer} className="answer">
<button onClick={() => handleSelectAnswer(answer)}>
{answer}
</button>
</li>
))}
</ul>
</div>
</div>
);
}
onTimeout 부분을 useCallback을 사용해야한다.handleSkipAnswer 함수로 만들었다. 또한 이 함수는 handleSelectAnswer을 이용해 상태를 업데이트 하므로 handleSelectAnswer을 의존성 추가해야 한다. → 해당 컴포넌트 함수에서 사용된 값 이니깐.handleSelectAnswer도 useCallback을 사용하되, 해당 함수는 컴포넌트에서 사용하는 상태나 속성이 없으므로 의존성을 추가하지 않는다.
import { useState, useEffect } from "react";
export default function QuestionTimer({ timeout, onTimeout }) {
const [remainingTime, setRemainingTime] = useState(timeout);
useEffect(() => {
console.log("SETTING TIMEOUT");
const timer = setTimeout(onTimeout, timeout);
return () => {
// summary에 들어가면 타이머도 사라져야 함.
clearTimeout(timer);
};
}, [onTimeout, timeout]);
useEffect(() => {
console.log("SETTING INTERVAL");
const interval = setInterval(() => {
setRemainingTime((prevRemainingTime) => prevRemainingTime - 100);
}, 100);
return () => {
// 클린업 함수는 Effect 함수를 다시 작동하기 전이나 컴포넌트가 DOM으로부터 삭제될 때(스크린에서 사라지면) 리액트에서 자동으로 실행됨.
clearInterval(interval);
};
}, []);
return <progress id="question-time" value={remainingTime} max={timeout} />;
}

export default function Quiz() {
return (
<div id="quiz">
<div id="question">
{/* key를 부여하여 타이머도 질문이 바뀔 때마다 업데이트(초기화) 되도록 함 */}
<QuestionTimer
key={activeQuestionIndex}
timeout={10000}
onTimeout={handleSkipAnswer}
/>
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
<ul id="answers">
{shuffledAnswers.map((answer) => (
<li key={answer} className="answer">
<button onClick={() => handleSelectAnswer(answer)}>
{answer}
</button>
</li>
))}
</ul>
</div>
</div>
);
}

import { useState, useCallback } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
import QuestionTimer from "./QuestionTimer.jsx";
export default function Quiz() {
const [answerState, setAnswerState] = useState("");
const [userAnswers, setUserAnswers] = useState([]);
const activeQuestionIndex =
answerState === "" ? userAnswers.length : userAnswers.length - 1; // 이전 질문에 머무르도록 함.
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
const handleSelectAnswer = useCallback(
function handleSelectAnswer(selectedAnswer) {
setAnswerState("answered"); // 사용자가 답변을 고른다면 해당 상태를 업데이트
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
setTimeout(() => {
if (selectedAnswer === QUESTIONS[activeQuestionIndex].answers[0]) {
// 정답이면
setAnswerState("correct");
} else {
// 오답이면
setAnswerState("wrong");
}
setTimeout(() => {
// 다시 답변을 초기화 함으로써 다음 질문으로 넘어가도록 함.
setAnswerState("");
}, 2000);
}, 1000); // 1초 뒤에 답변에 대한 클래스 네임 추가
},
[activeQuestionIndex]
); // 현재 QUESTIONS[activeQuestionIndex].answers[0]를 사용하므로 의존성 추가 필요.
// activeQuestionIndex 값이 변경될 때마다 재실행될 필요가 있다.
const handleSkipAnswer = useCallback(() => {
handleSelectAnswer(null);
}, [handleSelectAnswer]);
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
const shuffledAnswers = [...QUESTIONS[activeQuestionIndex].answers];
shuffledAnswers.sort(() => Math.random() - 0.5);
return (
<div id="quiz">
<div id="question">
<QuestionTimer
key={activeQuestionIndex}
timeout={10000}
onTimeout={handleSkipAnswer}
/>
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
<ul id="answers">
{shuffledAnswers.map((answer) => {
// 클래스 이름 부여
const isSelcted = userAnswers[userAnswers.length - 1] === answer;
let cssClasses = "";
if (answerState === "answered" && isSelcted) {
cssClasses = "selected";
}
if (
(answerState === "correct" || answerState === "wrong") &&
isSelcted
) {
cssClasses = answerState;
}
return (
<li key={answer} className="answer">
<button
onClick={() => handleSelectAnswer(answer)}
className={cssClasses}
>
{answer}
</button>
</li>
);
})}
</ul>
</div>
</div>
);
}
answerState)userAnswers.length로 설정한다. 그러나 만약 유저가 답변을 제출한 적이 있다면(첫 문제가 아니라면), activeQuestionIndex을 userAnswers.length-1로 설정하여 잠시동안 이전 문제에 머무르도록 한다.handleSelectAnswer 함수에서 상태를 업데이트한다.setAnswerState를 우선 answered로 설정한다.setUserAnswers를 업데이트한다.setAnswerState('correct')로 업데이트한다. 만약 일치하지 않는다면 setAnswerState('wrong')으로 업데이트한다.answerState를 빈 문자열로 추가한다. 이는 그 다음 문제로 넘어가기 위함이다.(activeQuestionIndex 이용함)activeQuestionIndex을 추가해야한다. 해당 인덱스가 바뀔 때마다 함수를 재실행할 필요가 있기 때문이다.shuffledAnswers.map()에서 클래스를 부여하기 위한 로직을 작성한다.shuffledAnswers의 answer 중 하나와 일치한다면 해당 답변은 isSelected = true가 된다.answerState가 answered이고 isSelcted===true라면 해당 답변 버튼의 클래스는 selected가 된다.answerState가 correct이거나 wrong이고 isSelected===true이면 해당 답변 버튼의 클래스는 answerState의 값이 된다.
import { useState, useCallback, useRef } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
import QuestionTimer from "./QuestionTimer.jsx";
export default function Quiz() {
const shuffledAnswers = useRef(); // 몇가지의 값만을 관리할 것.
const [answerState, setAnswerState] = useState("");
const [userAnswers, setUserAnswers] = useState([]);
const activeQuestionIndex =
answerState === "" ? userAnswers.length : userAnswers.length - 1;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
const handleSelectAnswer = useCallback(
function handleSelectAnswer(selectedAnswer) {
setAnswerState("answered");
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
setTimeout(() => {
if (selectedAnswer === QUESTIONS[activeQuestionIndex].answers[0]) {
setAnswerState("correct");
} else {
setAnswerState("wrong");
}
setTimeout(() => {
setAnswerState("");
}, 2000);
}, 1000);
},
[activeQuestionIndex]
);
const handleSkipAnswer = useCallback(() => {
handleSelectAnswer(null);
}, [handleSelectAnswer]);
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
// shuffledAnswerrs.current가 undefined일때. => 아직 shuffledAnswers = useRef()로 선언만 했을 뿐이다.
if (!shuffledAnswers.current) {
shuffledAnswers.current = [...QUESTIONS[activeQuestionIndex].answers];
shuffledAnswers.current.sort(() => Math.random() - 0.5);
}
return (
<div id="quiz">
<div id="question">
<QuestionTimer
key={activeQuestionIndex}
timeout={10000}
onTimeout={handleSkipAnswer}
/>
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
<ul id="answers">
{shuffledAnswers.current.map((answer) => {
const isSelcted = userAnswers[userAnswers.length - 1] === answer;
let cssClasses = "";
if (answerState === "answered" && isSelcted) {
cssClasses = "selected";
}
if (
(answerState === "correct" || answerState === "wrong") &&
isSelcted
) {
cssClasses = answerState;
}
return (
<li key={answer} className="answer">
<button
onClick={() => handleSelectAnswer(answer)}
className={cssClasses}
>
{answer}
</button>
</li>
);
})}
</ul>
</div>
</div>
);
}

// Answers.jx
import { useRef } from "react";
export default function Answers({
answers,
selectedAnswer,
answerState,
onSelect,
}) {
const shuffledAnswers = useRef(); // 몇가지의 값만을 관리할 것.
// shuffledAnswerrs.current가 undefined일때. => 아직 shuffledAnswers = useRef()로 선언만 했을 뿐이다.
if (!shuffledAnswers.current) {
shuffledAnswers.current = [...answers];
shuffledAnswers.current.sort(() => Math.random() - 0.5);
}
return (
<ul id="answers">
{shuffledAnswers.current.map((answer) => {
const isSelcted = selectedAnswer === answer;
let cssClasses = "";
if (answerState === "answered" && isSelcted) {
cssClasses = "selected";
}
if (
(answerState === "correct" || answerState === "wrong") &&
isSelcted
) {
cssClasses = answerState;
}
return (
<li key={answer} className="answer">
<button onClick={() => onSelect(answer)} className={cssClasses}>
{answer}
</button>
</li>
);
})}
</ul>
);
}
// Quiz.jsx
import { useState, useCallback } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
import QuestionTimer from "./QuestionTimer.jsx";
import Answers from "./Answers.jsx";
export default function Quiz() {
const [answerState, setAnswerState] = useState("");
const [userAnswers, setUserAnswers] = useState([]);
const activeQuestionIndex =
answerState === "" ? userAnswers.length : userAnswers.length - 1;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
const handleSelectAnswer = useCallback(
function handleSelectAnswer(selectedAnswer) {
setAnswerState("answered");
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
setTimeout(() => {
if (selectedAnswer === QUESTIONS[activeQuestionIndex].answers[0]) {
setAnswerState("correct");
} else {
setAnswerState("wrong");
}
setTimeout(() => {
setAnswerState("");
}, 2000);
}, 1000);
},
[activeQuestionIndex]
);
const handleSkipAnswer = useCallback(() => {
handleSelectAnswer(null);
}, [handleSelectAnswer]);
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
return (
<div id="quiz">
<div id="question">
{/* handleSelectAnswer(null)로 설정함으로써 해당 질문에 어떠한 답변하지 않고 넘어갔음을 상태에 알림 */}
<QuestionTimer
key={activeQuestionIndex}
timeout={10000}
onTimeout={handleSkipAnswer}
/>
<h2>{QUESTIONS[activeQuestionIndex].text}</h2>
{/* key를 통해서 리액트가 컴포넌트를 삭제하고 재생성할 수 있게 함. -> 셔플 */}
{/* 같은 div에서 같은 key를 사용하면 안된다. */}
<Answers
key={activeQuestionIndex}
answers={QUESTIONS[activeQuestionIndex].answers}
selectedAnswer={userAnswers[userAnswers.length - 1]}
answerState={answerState}
onSelect={handleSelectAnswer}
/>
</div>
</div>
);
}
// =========================== Quiz.jsx ===========================
import { useState, useCallback } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
import Question from "./Question.jsx";
export default function Quiz() {
const [answerState, setAnswerState] = useState("");
const [userAnswers, setUserAnswers] = useState([]);
const activeQuestionIndex =
answerState === "" ? userAnswers.length : userAnswers.length - 1;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
const handleSelectAnswer = useCallback(
function handleSelectAnswer(selectedAnswer) {
setAnswerState("answered");
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
setTimeout(() => {
if (selectedAnswer === QUESTIONS[activeQuestionIndex].answers[0]) {
setAnswerState("correct");
} else {
setAnswerState("wrong");
}
setTimeout(() => {
setAnswerState("");
}, 2000);
}, 1000);
},
[activeQuestionIndex]
);
const handleSkipAnswer = useCallback(() => {
handleSelectAnswer(null);
}, [handleSelectAnswer]);
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
return (
<div id="quiz">
<Question
// 이제 Question 컴포넌트만 업데이트하면 하위 컴포넌트는 자동 업데이트 된다.
key={activeQuestionIndex}
questionText={QUESTIONS[activeQuestionIndex].text}
answers={QUESTIONS[activeQuestionIndex].answers}
onSelectAnswer={handleSelectAnswer}
selectedAnswer={userAnswers[userAnswers.length - 1]}
answerState={answerState}
onSkipAnswer={handleSkipAnswer}
/>
</div>
);
}
// =========================== Question.jsx ===========================
import QuestionTimer from "./QuestionTimer.jsx";
import Answers from "./Answers.jsx";
export default function Question({
questionText,
answers,
onSelectAnswer,
selectedAnswer,
answerState,
onSkipAnswer,
}) {
return (
<div id="question">
<QuestionTimer
timeout={10000}
onTimeout={onSkipAnswer}
/>
<h2>{questionText}</h2>
<Answers
answers={answers}
selectedAnswer={selectedAnswer}
answerState={answerState}
onSelect={onSelectAnswer}
/>
</div>
);
}
// =========================== Answers.jsx ===========================
import { useRef } from "react";
export default function Answers({
answers,
selectedAnswer,
answerState,
onSelect,
}) {
const shuffledAnswers = useRef(); // 몇가지의 값만을 관리할 것.
// shuffledAnswerrs.current가 undefined일때. => 아직 shuffledAnswers = useRef()로 선언만 했을 뿐이다.
if (!shuffledAnswers.current) {
shuffledAnswers.current = [...answers];
shuffledAnswers.current.sort(() => Math.random() - 0.5);
}
return (
<ul id="answers">
{shuffledAnswers.current.map((answer) => {
const isSelcted = selectedAnswer === answer;
let cssClasses = "";
if (answerState === "answered" && isSelcted) {
cssClasses = "selected";
}
if (
(answerState === "correct" || answerState === "wrong") &&
isSelcted
) {
cssClasses = answerState;
}
return (
<li key={answer} className="answer">
<button onClick={() => onSelect(answer)} className={cssClasses}>
{answer}
</button>
</li>
);
})}
</ul>
);
}

import QuestionTimer from "./QuestionTimer.jsx";
import Answers from "./Answers.jsx";
import QUESTIONS from "../questions.js";
import { useState } from "react";
export default function Question({
questionIdx,
onSelectAnswer,
onSkipAnswer,
}) {
const [answer, setAnswer] = useState({
selectedAnswer: "",
isCorrect: null,
});
function handleSelectAnswer(answer) {
setAnswer({
selectedAnswer: answer,
isCorrect: null, // 아직 correct인지 wrong인지 모름 -> 1초 정도 뒤에 다시 업데이트를 해야한다.(Quiz 컴포넌트의 영향)
});
setTimeout(() => {
setAnswer({
selectedAnswer: answer,
isCorrect: answer === QUESTIONS[questionIdx].answers[0],
});
setTimeout(() => {
onSelectAnswer(answer);
}, 2000);
}, 1000);
}
let answerState = "";
if (answer.selectedAnswer && answer.isCorrect !== null) {
answerState = answer.isCorrect ? "correct" : "wrong";
} else if (answer.selectedAnswer) {
answerState = "answered";
}
return (
<div id="question">
<QuestionTimer timeout={10000} onTimeout={onSkipAnswer} />
<h2>{QUESTIONS[questionIdx].text}</h2>
<Answers
answers={QUESTIONS[questionIdx].answers}
selectedAnswer={answer.selectedAnswer}
answerState={answerState}
onSelect={handleSelectAnswer}
/>
</div>
);
}
import { useState, useCallback } from "react";
import QUESTIONS from "../questions.js";
import quizComplteImg from "../assets/quiz-complete.png";
import Question from "./Question.jsx";
export default function Quiz() {
const [userAnswers, setUserAnswers] = useState([]);
const activeQuestionIndex = userAnswers.length;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
const handleSelectAnswer = useCallback(function handleSelectAnswer(
selectedAnswer
) {
setUserAnswers((prevUserAnswers) => {
return [...prevUserAnswers, selectedAnswer];
});
},
[]);
const handleSkipAnswer = useCallback(() => {
handleSelectAnswer(null);
}, [handleSelectAnswer]);
if (quizIsComplete) {
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
</div>
);
}
return (
<div id="quiz">
<Question
// 이제 Question 컴포넌트만 업데이트하면 하위 컴포넌트는 자동 업데이트 된다.
key={activeQuestionIndex}
questionIdx={activeQuestionIndex}
onSelectAnswer={handleSelectAnswer}
onSkipAnswer={handleSkipAnswer}
/>
</div>
);
}
// Answers.jsx
<button
onClick={() => onSelect(answer)}
className={cssClasses}
disabled={answerState !== ""}
>
{answer}
</button>

import QuestionTimer from "./QuestionTimer.jsx";
import Answers from "./Answers.jsx";
import QUESTIONS from "../questions.js";
import { useState } from "react";
export default function Question({
questionIdx,
onSelectAnswer,
onSkipAnswer,
}) {
const [answer, setAnswer] = useState({
selectedAnswer: "",
isCorrect: null,
});
let timer = 10000;
if (answer.selectedAnswer) {
timer = 1000; // 1초 뒤 정답/오답 보여주기 위함
}
if (answer.isCorrect !== null) {
timer = 2000; // 다음 질문으로 넘어가는데 걸리는 시간
}
function handleSelectAnswer(answer) {
setAnswer({
selectedAnswer: answer,
isCorrect: null, // 아직 correct인지 wrong인지 모름 -> 1초 정도 뒤에 다시 업데이트를 해야한다.(Quiz 컴포넌트의 영향)
});
setTimeout(() => {
setAnswer({
selectedAnswer: answer,
isCorrect: answer === QUESTIONS[questionIdx].answers[0],
});
setTimeout(() => {
onSelectAnswer(answer);
}, 2000);
}, 1000);
}
let answerState = "";
if (answer.selectedAnswer && answer.isCorrect !== null) {
answerState = answer.isCorrect ? "correct" : "wrong";
} else if (answer.selectedAnswer) {
answerState = "answered";
}
return (
<div id="question">
<QuestionTimer
key={timer}
timeout={timer}
onTimeout={answer.selectedAnswer === "" ? onSkipAnswer : null}
mode={answerState}
/>
<h2>{QUESTIONS[questionIdx].text}</h2>
<Answers
answers={QUESTIONS[questionIdx].answers}
selectedAnswer={answer.selectedAnswer}
answerState={answerState}
onSelect={handleSelectAnswer}
/>
</div>
);
}
import { useState, useEffect } from "react";
export default function QuestionTimer({ timeout, onTimeout, mode }) {
const [remainingTime, setRemainingTime] = useState(timeout);
useEffect(() => {
console.log("SETTING TIMEOUT");
const timer = setTimeout(onTimeout, timeout);
return () => {
clearTimeout(timer);
};
}, [onTimeout, timeout]);
useEffect(() => {
console.log("SETTING INTERVAL");
const interval = setInterval(() => {
setRemainingTime((prevRemainingTime) => prevRemainingTime - 100);
}, 100);
return () => {
clearInterval(interval);
};
}, []);
return (
<progress
id="question-time"
value={remainingTime}
max={timeout}
className={mode}
/>
);
}

import quizComplteImg from "../assets/quiz-complete.png";
import QUESTIONS from "../questions.js";
export default function Summary({ userAnswers }) {
const skippedAnswers = userAnswers.filter((answer) => answer === null);
const correctAnswers = userAnswers.filter(
(answer, index) => answer === QUESTIONS[index].answers[0]
);
const skippedAnswersShare = Math.round(
(skippedAnswers.length / userAnswers.length) * 100
);
const correctAnswersShare = Math.round(
(correctAnswers.length / userAnswers.length) * 100
);
const wrongAnswersShare = 100 - skippedAnswersShare - correctAnswersShare;
return (
<div id="summary">
<img src={quizComplteImg} alt="Trophy icon" />
<h2>Quiz Completed!</h2>
<div id="summary-stats">
<p>
<span className="number">{skippedAnswersShare}%</span>
<span className="text">skipped</span>
</p>
<p>
<span className="number">{correctAnswersShare}%</span>
<span className="text">answered correctly</span>
</p>
<p>
<span className="number">{wrongAnswersShare}%</span>
<span className="text">answered incorrectly</span>
</p>
</div>
<ol>
{userAnswers.map((answer, index) => {
let cssClass = "user-answer";
if (answer === null) {
// skipped
cssClass += " skipped";
} else if (answer === QUESTIONS[index].answers[0]) {
cssClass += " correct";
} else {
cssClass += " wrong";
}
return (
<li key={index}>
<h3>{index + 1}</h3>
<p className="question">{QUESTIONS[index].text}</p>
<p className={cssClass}>{answer ?? "Skipped"}</p>
</li>
);
})}
</ol>
</div>
);
}
if (quizIsComplete) {
return <Summary userAnswers={userAnswers} />;
}

key값을 이용하면 된다는 것을 깨달았다.