왜 답변을 고를 때마다
이 부분이 계속 바뀔까요?
이 이유는 여기 있습니다
바로 Quiz 컴포넌트입니다
규모가 점점 커지고 있는데
이 Quiz 컴포넌트에서 답변을 섞고 있기 때문입니다
무작위로 섞는 작업을 여기에서 하고 있고
그리고 이 코드는
이 Quiz 컴포넌트 함수가 실행될 때마다 실행됩니다
그러므로, 이것에는 문제가 없었습니다
왜냐면 이 함수는 상태가 변경될 때에만
다시 실행되기 때문이고
상태는 다음 문제로 넘어갈 때에만 변경되었기 때문입니다
하지만 이 상태는 변경되었습니다
상태 사이사이에 선택된 답변을
강조 표시하기 시작했습니다
그리고 이 때문에 새로운 질문으로 바로 넘어가지 않습니다
그리하여 이 Quiz 컴포넌트 함수는
asnwerState를 변경할 때마다
다시 실행되는 것입니다
즉 그러므로 저 답변을 다시 섞게 됩니다
답변의 새로운 배열을 생성하는 것입니다
그리고 이는 문제가 됩니다
답변들을 오직 한번만 섞어야 하기 때문입니다
이를 이루기 위해서 두 가지 방식이 있습니다
한 가지 방식은 새로운 상태를 추가하는 겁니다
바로 shuffledAnswers 상태입니다
처음엔 텅 빈 배열이겠지만
shuffledAnswers 상태를 넣고
setsuffledAnswers 상태 업데이트 함수를 넣습니다
그리고 나서 Effect 훅을 사용하여
이 컴포넌트가 최초로 렌더링 될 때에나
activeQuestionIndex가 변경될 때만
업데이트 되도록 할 수 있습니다
activeQuestionIndex를 useEffect의
의존성으로 추가함으로 할 수 있습니다
이게 하나의 방식입니다 하지만
이 방식을 사용하지 않겠습니다
왜냐하면 리액트 개발자로서
useEffect의 사용을
최소화 해야 하기 때문입니다
왜냐하면 이는 비교적으로 빠르게 진행되므로
useEffect를 잘못 사용하면
잠재적으로 앱에 버그를 추가할 수 있기 때문입니다
그러므로, 가능하다면 useEffect의 사용을 피하는 것은
바람직한 연습의 방향입니다
그리고 이렇게 하면 피할 수 있습니다
왜냐면 useref 훅을 이용해서 ref (참조)를 사용할 수 있기 때문입니다
HTML 요소를 연결하지 않기 위해
하지만 대신에 몇 가지값을 관리하기 위해
이전 강의에서 배우신 것처럼
컴포넌트 함수가 다시 실행된다면 바뀌지 않을 겁니다
이 부분에 대해서
참조를 다룰 때 설명드렸습니다
각자 속하는 컴포넌트 함수 사이클로부터
독립적으로 보관되고 관리되는값을
관리하기 위해 참조를 사용할 수 있다는 것을 배웠습니다
그래서 여기에 shuffledAnswersref를 가질 수 있습니다
이름은 임의로 설정하시면 됩니다
그리고 아래의 상수 대신에
이 참조에 shuffledAnswers를 보관할 수 있습니다
해당 배열와 같은 shuffledAnswers.current 값을
설정합니다
그리고 shuffledAnswers.current sort를 호출합니다
그리고 나서 이 로직을 if check로 감쌉니다
shuffledAnswers.current가
정의되지 않은 상태(undefined)인지 확인하는 것입니다
True가 아니라면 그러하므로 이 때에는 이 코드를 실행하겠습니다
즉 Undefined의 경우에
아직 shuffledAnswers가 없다는 것을 알고 있습니다
왜냐하면 undefined가 초기 상태이기 때문입니다
그렇지만 한번 정의 되면
다시는 섞지 않겠습니다
만약 컴포넌트 함수가 다시 실행된다고 해도 그렇습니다
왜냐면 이미 순서를 바꿨기 때문입니다
그래서 그렇게 하면, 아래 JSX 코드에서도
shuffledAnswers.current를
사용하도록 해야 합니다
그리고 이를 저장하고 다시 불러오면
그리고 답변을 고르면
답변 창이 자리에 머물고
다시 섞지 않는 것을 볼 수 있습니다
그리고 답변이 선택되면서 강조 표시가 나타나고
정답 혹은 오답인지 보여주기도 합니다
하지만 또한 확인할 수 있는 점은
답변을 선택하고 다음 질문으로 넘어갈 때
답변이 골라진 상태로 유지 됩니다
그리고 더 안 좋은 것은
답변이 더 이상 바뀌지 않는 점입니다
이는 새로운 질문을 받는 시점입니다
그래서 앱이 더이상 올바르게 작동하지 않습니다
그리고 이제 이런 일이 일어나는 이유는 참조의 사용법 때문입니다
이제 섞인 답변들을 다시 바꾸고 있지 않는데
한 번만 섞고 멈추기 때문입니다
그래서 이제 이를 해결하는
가장 쉬운 방법은
제 의견으로는 새로운 컴포넌트를 만드는 겁니다
왜냐하면 리액트의 핵심은 컴포넌트이기 때문입니다
이 부분은 컴포넌트 사용이
왜 좋은 방식인지, 그리고 왜 이것으로
복잡한 문제를 해결할 수 있는지 보여줍니다
여기에 새로운 컴포넌트를 추가합니다
Answers 컴포넌트입니다
이는 Answers.jsx (정답 파일)에 있습니다
이는 기본 컴포넌트 함수입니다
여기에서 내보내겠습니다
그리고 이 컴포넌트의 목적은
섞인 답변의 목록을 출력하는 것입니다
그래서 여기 순서 없는 목록을 가져오겠습니다
바로 이 여기 정답에 대한 순서 없는 목록인데
Quiz 컴포넌트에서 잘라냅니다
그리고 여기 정답 파일에 반환값으로 추가합니다
이제 이 Answers 컴포넌트에
답변들을 올바르게 렌더링 하기 위해서
여러 정보가 필요합니다
예를 들어, 렌더링 되어야 하는
답변의 목록들이 필요할 겁니다
그래서 답변의 배열을 입력값, 즉 속성으로 받아야합니다
추가적으로, 어떤 답변이 선택되었는지 알 필요가 있습니다
그렇기에 우리가 사용할 수 있는
selectedAnswer 속성을 얻는다면 좋습니다
그리고 저 answerState가 필요합니다
그래서 답변이 선택이 되었는지
그리고 선택된 답변이 정답인지 아닌지를
알 수 있게 됩니다
그래서 Quiz 컴포넌트로 돌아가서
우리는 이 새롭게 추가된 Answers 컴포넌트를
정답 파일로부터 가져와서
그리고 아래 QUESTIONS text에
출력하겠습니다
출력되어야 하는 이 Answers는
섞인 답변들이 아닙니다
왜냐하면 해당 로직을
여기 Answers 컴포넌트 안에 추가할 것이기 때문입니다
대신 선택된 질문들에 대해
이용 가능한 답변을 출력하겠습니다
여기의 값, Questions의
activeQuestionIndex.answers로
선택된 답변에 대한값은 selectedAnswer이어야 합니다
userAnswers 내 보관된 최신 답변들을
여기 파생한 것 처럼
그 값이 바로 selected answer로서 전달하는 값입니다
그리고 answerState는
여기 퀴즈 컴포넌트에서 관리하고 있는 answerState입니다
이제 언급한 것처럼
무작위로 섞는 로직은 이제
Answers 컴포넌트로 옮겨져야 합니다
그러니 Quiz 컴포넌트로부터 잘라냅니다
그리고 여기에 추가하겠습니다
이는 또한 뜻하는 바가
참조는 Answers 컴포넌트에 옮겨져야 하는 다는 것입니다
그러니 Quiz 컴포넌트에서 잘라서
여기 Answers 컴포넌트 안에 추가합니다
그리고 이제 여기에 useRef를 리액트로부터 가져와야 합니다
다른 관점에서, 이 Quiz 컴포넌트에서
이 import문은 삭제하겠습니다
그리고 여기 Answers 컴포넌트에서
suffledAnswer는 여기 속성으로 받고 있는
답변에 따라서 파생되어야 합니다
그래서 여기 답변들을 이처럼 펼칩니다
여기의 aswers는
그리고 selectedAnswer은 아래에서 사용됩니다
그리고 또한 답변이 선택되면
Quiz 컴포넌트로 보내야 합니다
그렇기 때문에
onSelect 속성 또한 넣어줍니다
이름은 임의로 정하시면 됩니다
함수를 값으로 받을 것이며
아래에 함수로서 호출되어야 합니다
이제 Quiz 컴포넌트로 돌아가서
여기 또한 onSelect를 추가해줍니다
그리고 handleSelectAnswer로 가리킵니다
그리고 Answers 컴포넌트가
선택된 답변들이 onSelect로값으로서
전달되도록 할 것이기 때문에
이제 여기 퀴즈 컴포넌트에서 handleSelectAnswer로
직접적으로 가리킬 수 있습니다
Answers 컴포넌트가
이 함수에 올바른값을 전달할 것이기 때문이죠
그런데 이것이 지금 섞는 것에 어떻게 도움이 될까요?
중요한 부분입니다
결과적인 목표는 다시 섞는 것입니다
질문이 바뀌는 시점의 이야기입니다
그래서 activeQuestionIndex가 바뀌면서도 마찬가지입니다
만일 Answers 컴포넌트가 questionIndex가 바뀌면서
재생성되도록
즉 예전의 것이 삭제되고
그리고 새로운 인스턴스가 생성되도록
작업하겠습니다
삭제되거나 재생성되면
여기 있는 모든 코드가 재실행될 것이기 때문입니다
그리고 이것이 각각의 컴포넌트에 두게 될 때의
이점이라고 할 수 있습니다
왜냐하면 다시 키 속성을 통해
비교적 쉽게 작업할 수 있기 때문입니다
이 부분은 questionTimer에 대해서 설명을 드렸습니다
키 속성을 통해서 리액트가 컴포넌트를
삭제하고 재생성 할 수 있습니다
activeQuestionIndex가 변경될 때마다
여기 답변을 위해 하고자 하는 것입니다
타이머를 가지고 했던 것과 같습니다
그렇기에 이제, 그 로직을 각각의 컴포넌트로
아무것도 바꾸는 것 없이
옮기는 것만으로
여기 좋은 방법을 이용해서
컴포넌트로 보내고
키를 여기에 추가하면서
문제점을 없앴습니다
이제, 지금 발견한 이 속성을
고쳐야 합니다
Answers 컴포넌트에 있는 answersState는
answers가 아니라 AnswerState로 불려야 합니다
그러나 이렇게 해서 새로고침 하고 답을 고르면
섞이지 않는 것을 볼 수 있습니다
그렇지만 다른 답변으로 변경하면 초기화 합니다
그리고 또한 새로운 질문으로 넘어가면서
이상한 버그가 있다는 것을
눈치 채셨을 겁니다
진행 표시줄이 두 개가 되었습니다
개발자 도구를 열어보면
경고 메시지가 뜬 것을 볼 수 있을 겁니다
같은 키로 다수의 요소를
사용하고 있다는 문구죠
이것이 여기서 일어나는 일입니다
같은 키인 activeQuestionIndex를
두 개의 다른 컴포넌트에 사용하고 있습니다
그리고 이것은 해서는 안 되는 일입니다
여기에 각기 다른
다른 종류의 컴포넌트가 있고
그들은 형제 컴포넌트입니다
같은 디브에서 같은 키를 사용합니다
리액트가 피하려고 하는 것이 이런 현상입니다
그래서 이것을 고치기 위해 무엇을 할 수 있을까요?
우리가 할 수 있는 한 가지 작업은
이 문제에 대해 적절한 해결방법입니다
바로 컴포넌트 하나를 더 만들어 내는 겁니다
왜냐하면 이미 보시듯이
컴포넌트를 추가하는 것은 도움이 되는 것을 넘어서
코드를 쉽게 관리할 수 있게 해주고
또한 문제를 해결하게 도와줍니다
로직을 나누고 합치면서
충돌을 방지하기 때문입니다
이제 새로운 Question 컴포넌트를 여기에 추가할 겁니다
또한 당연히 export문을 쓰겠습니다
그리고 여기에 디브를 반환하는데
question의 ID와 함께 하겠습니다
그리고 여기의 모든 코드를
마지막의 디브까지
Question 컴포넌트로 옮겨줍니다
그리고 여기의 디브를 교체합니다
그리고 이제 QuestionTimer를 가져오겠습니다
그리고 Answers 또한 가져오겠습니다
그리고 여기 Question 컴포넌트에
몇 가지 입력값이 필요합니다
예를들어, questionText가 필요합니다
answers도 필요합니다
onSelect Answer 속성으로 답변이 선택된 것인지
구분하기 위한 방법으로
종합적인 Quiz 컴포넌트가 필요합니다
그리고 이제 여기 Quiz 컴포넌트에서
이 Question 컴포넌트를 사용해야 합니다
questionTimer와 answer를 가져오는 대신
Questions.jsx (문제 파일)로부터 question을 가져올 수 있습니다
그리고 여기 아래에 이 컴포넌트를 이처럼 사용할 수 있습니다
이제 questionText가
QUESTIONS[activeQuestionIndex].text와 일치할 겁니다
그래서 이것을 문제 파일로부터 잘라내서
퀴즈 파일로 옮겨야 합니다
answers를 Questions[activeQuestionIndex].answers와
같게 설정합니다
그리고 이를 문자 파일에서 잘라내서 퀴즈 파일로 옮깁니다
그리고 onSelectAnswer = handleSelectAnswer를 설정합니다
그래서 그 함수는 지금은 여기에 놔두겠습니다
이제 Question 컴포넌트에서 answers를 전달해야 합니다
바로 Answer 컴포넌트로 전달하는데
questionText를 여기 사용하고
여기 onSelect의 값으로 onSelectAnswer를 넣습니다
그러나 이제 마지막 selectedAnswer가 필요하고
answerState가 여기에 필요할 겁니다
Answers 컴포넌트를 정확하게 업데이트하기 위해서입니다
그러므로 여기에 selectedAnswer를 가져오고
answerState를 속성로서 가져옵니다
그래서 이 컴포넌트를 사용할 수 있도록 하겠습니다
그리고 selectedAnswer를 여기에 사용합니다
여기 이 userAnswers 길이 배열과
마지막 요소를 선택하는 것에 대한 대안입니다
그리고 Quiz 컴포넌트로 돌아가서
이제 또한 속성을 question에 설정해야 하고
answerState를
answerState를 동격으로 설정하고
selectedAnswer를 uswerAnswers에 동격으로 설정합니다
그리고 그들이 마지막 요소입니다
그리고 Question 컴포넌트에서
이제 우리는 handleSkipAnswer도 있습니다
이는 또한 속성을 통해 받아져야 하는데
onSkipAnswer이 속성의 이름이 될 수 있습니다
그러고 나서 onTimeout의 값로서 설정합니다
여기 Quiz 컴포넌트에서
그러고 나서 onSkipAnswer를 추가합니다
그리고 나서 handleSkipAnswer로 가리킵니다
이것이 어떻게 키로 인한 문제를 해결할까요?
이제 여기의 키는 삭제할 수 있습니다
Question 컴포넌트에서
그리고 대신에 여기 퀴즈 파일의 Question 컴포넌트에서
키를 사용합니다
왜냐하면 결국에 이 Question 전체가
초기화 되어야 하기 때문입니다
activeQuestionIndex가 바뀔 때마다 바뀌어야 합니다
이렇게 해서 저장을 하고 다시 불러오면
에러가 사라진 것을 볼 수 있습니다
그리고 답변을 하나 고르면, 이전처럼 잘 됩니다
그런데 다음 질문으로 넘어가면 초기화 되고
여기서 또한 에러가 뜨지 않습니다
그리고 이제 답변을 계속 고를 수 있습니다
그리고 다른 질문들을 답변해나갈 수도 있습니다
이제 작동해야 하는 대로 잘 작동합니다