리팩토링 - (3) Create Question

jiny·2022년 8월 18일
0

리팩토링 전 create Question

CreateQuestion.tsx

import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useRecoilState, useSetRecoilState } from "recoil";
import { Container, CommonComponent, Title, SubTitle, HeadTitle, PrimaryLargeButton, CheckSubComponent, Answer, BackCircle } from "../components/commons/Commons";
import { CommonInput } from "../components/commons/Input";
import { correctNum, incorrectNum, questionNum, questionSet } from "../utils/storage";

function CreateQuestion() {
    // Question 갯수는 check 페이지에서도 써야하기 때문에 useRecoilState로 관리
    const [allQuestion, setQuestionCount] = useRecoilState(questionNum);

    // 문제와 답의 state
    const [question, setQuestion] = useState('');
    const [answer, setAnswer] = useState('');
    const addQuestion = (e:React.ChangeEvent<HTMLInputElement>) => {
        setQuestion(e.currentTarget.value);
    }
    const addAnswer = (e:React.ChangeEvent<HTMLTextAreaElement>) => {
        setAnswer(e.currentTarget.value);
    }

    // 문제-답(1세트) state
    const setSet = useSetRecoilState(questionSet);
    // 문제 생성 버튼
    const handleCreateQuestion = (question:string, answer:string) => {
        if(question === '' || answer === '') {
            alert("문제나 답을 다시 입력해주세요 :)");
            return
        }
        setSet((oldSet) => {
            const newSet = {id:Date.now(), question: question, answer: answer};
            return [newSet, ...oldSet];
        })
        // 문제 갯수 counting
        setQuestionCount((prevNum:number) => prevNum+1);
        // 생각해봐야 할 점
        // 1. 버튼을 누르면 textArea와 input의 있는 text들이 초기화 되어야 함.
        setQuestion('');
        setAnswer('');
        window.location.reload();
    }

    const correctQuestionsNum = useSetRecoilState(correctNum);
    const incorrectQuestionsNum = useSetRecoilState(incorrectNum);

    // 리셋 버튼을 누를 시, 문제 갯수, 정답, 오답 갯수 모두 초기화
    const handleResetButton = () => {
        correctQuestionsNum(0);
        incorrectQuestionsNum(0);
        setQuestionCount(0);
        setSet((prevQuestions) => {
            return prevQuestions = [];
        })
    }

    // 자가 점검 페이지로 이동
    const selfCheckPage = useNavigate();
    const goToSelfCheck = () => {
        selfCheckPage("/self-check")
    }

    // 문제 수정 페이지로 이동
    const modifyQuestionPage = useNavigate()
    const goModifyQuestion = () => {
        modifyQuestionPage("/modify");
    }

    // 뒤로가기 버튼
    const moveBack = useNavigate();
    const handleBackCircleClick = () => {
        moveBack('/check');
    }

    return (
        <Container style={{height: '150vh'}}>
            <BackCircle
                onClick = {handleBackCircleClick}
            >
                <img style={{marginRight:'2px', width: '50%'}} src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNzUycHQiIGhlaWdodD0iNzUycHQiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDc1MiA3NTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiA8ZGVmcz4KICA8Y2xpcFBhdGggaWQ9ImEiPgogICA8cGF0aCBkPSJtMjQzIDEzOS4yMWgyNjZ2NDczLjU4aC0yNjZ6Ii8+CiAgPC9jbGlwUGF0aD4KIDwvZGVmcz4KIDxnIGNsaXAtcGF0aD0idXJsKCNhKSI+CiAgPHBhdGggZD0ibTI1MS41OCAzNTQuNzMgMjA3LjAyLTIwNy4wMmMxMi4wNTEtMTEuMzQ0IDMwLjQ4NC0xMS4zNDQgNDEuODI4IDBzMTEuMzQ0IDI5Ljc3NyAwIDQxLjgyOGwtMTg2LjQ1IDE4Ni40NSAxODYuNDUgMTg2LjQ1YzExLjM0NCAxMS4zNDQgMTEuMzQ0IDMwLjQ4NCAwIDQxLjgyOHMtMjkuNzc3IDExLjM0NC00MS44MjggMGwtMjA3LjAyLTIwNy43MmMtMTEuMzQ0LTExLjM0NC0xMS4zNDQtMjkuNzc3IDAtNDEuODI4eiIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiA8L2c+Cjwvc3ZnPgo=" />
            </BackCircle>
            <Title>CREATE QUESTION</Title>
            <SubTitle>체크하고 싶은 문제를 만들어 보세요 :) </SubTitle>
            <SubTitle>문제들을 다 풀었다면 문제 리셋을 한 후 "점검 하러 가기"로 이동해주세요 :) </SubTitle>
            <CommonComponent style={{padding:'0px' ,width:'75%',height: '100vh'}}>
                <HeadTitle>Create Question</HeadTitle>
                <CheckSubComponent
                >
                    <CommonInput
                        onChange={addQuestion}
                        style={{borderRadius: '10px', marginBottom:'20px'}} 
                        placeholder="문제를 생성해 보세요 :)"
                    />
                    <Answer
                        onChange={addAnswer}
                        placeholder="문제에 맞는 답을 적어 주세요 :)"/>
                    <SubTitle style={{color:'black', margin:'10px'}}> Question : {allQuestion} </SubTitle>
                    <PrimaryLargeButton
                        onClick={() => handleCreateQuestion(question, answer)}
                    >
                        문제 생성
                    </PrimaryLargeButton>
                    <PrimaryLargeButton
                        onClick={goModifyQuestion}>문제 수정
                    </PrimaryLargeButton>
                    <PrimaryLargeButton
                        onClick={handleResetButton}>문제 리셋
                    </PrimaryLargeButton>
                    <PrimaryLargeButton
                        onClick={goToSelfCheck}>점검 하러 하기
                    </PrimaryLargeButton>
                </CheckSubComponent>
            </CommonComponent>
        </Container>
    )
}

export default CreateQuestion;

리팩토링 후 Create Question

CreateQuestionComponent.tsx

import BackButton from "src/components/commons/BackButton";
import { Container, CommonComponent, Title, SubTitle, HeadTitle, CheckSubComponent } from "src/components/commons/Commons";
import Input from "src/components/create/atoms/Input";
import TextArea from "src/components/create/atoms/TextArea";
import QuestionNumber from "src/components/create/atoms/QuestionTitle";
import useLoadAllQuestion from "src/hooks/useLoadAllQuestion";
import useCreate from "src/hooks/useCreate";
import CreateButton from "src/components/create/atoms/CreateButton";
import MovePageButton from "src/components/create/atoms/MovePageButton";
import ResetButton from "src/components/create/atoms/ResetButton";
import useReset from "src/hooks/useReset";

export default function CreateQuestionComponent() {

    const {questionNumber} = useLoadAllQuestion();

    const {handleCreateQuestion, question, answer} = useCreate();

    const {handleResetButton} = useReset();

    return (
        <Container style={{height: '150vh'}}>
            <BackButton url={"/check"}/>
            <Title>CREATE QUESTION</Title>
            <SubTitle>체크하고 싶은 문제를 만들어 보세요 :) </SubTitle>
            <SubTitle>문제들을 다 풀었다면 문제 리셋을 한 후 "점검 하러 가기"로 이동해주세요 :) </SubTitle>
            <CommonComponent style={{padding:'0px' ,width:'75%',height: '100vh'}}>
                <HeadTitle>Create Question</HeadTitle>
                <CheckSubComponent>
                    <Input/>
                    <TextArea/>
                    <QuestionNumber questionNumber={questionNumber}/>
                    <CreateButton
                        createQuesitonFn = {handleCreateQuestion}
                        content = {"문제 생성"}
                        question = {question}
                        answer = {answer}
                    />
                    <MovePageButton
                        url={"/modify"}
                        content={"문제 수정"}
                    />
                    <ResetButton
                        resetFn={handleResetButton}
                        content={"문제 리셋"}
                    />
                    <MovePageButton
                        url={"/self-check"}
                        content={"점검하러 가기"}
                    />
                </CheckSubComponent>
            </CommonComponent>
        </Container>
    )
}
  • 각 기능(전체 문제 개수, 문제 생성 함수, 리셋 함수)들을 custom hook으로 분리하여 SRP를 지키고자 함

  • 느슨한 결합을 위해 Input, TextArea, QuestionNumber, CreateButton, ResetButton, MovePageButton을 atom으로 분리

  • atom 들을 CreateQuestionComponent에 결합

Input & TextArea

input.tsx & textarea.tsx

import { useSetRecoilState } from "recoil";
import { questionState } from "src/utils/storage";
import styled from "styled-components";

export const CommonInput = styled.input`
    width: 70%;
    height: 10%;
    border-radius: 10px;
    text-align: center;
    border: 0px;
    background-color: #E0E3E8;
    box-shadow: 0 4px 6px rgba(50, 50, 93, 0.1), 0 1px 3px rgba(0, 0, 0, 0.05);
    margin-bottom: 20px;
    margin-top : 10px;
`

interface InputProps {
    changeFn : (e: React.ChangeEvent<HTMLInputElement>) => void;
}

export default function Input ({changeFn} : InputProps) {
    return (
            <CommonInput
                onChange={changeFn}
                placeholder="문제를 생성해 보세요 :)"
            />
    )
}
import styled from "styled-components";


export const Answer = styled.textarea`
    width: 70%;
    height: 70px;
    border : 0px;
    box-shadow: 0 4px 6px rgba(50, 50, 93, 0.15), 0 1px 3px rgba(0, 0, 0, 0.08);
    border-radius: 10px;
    text-align: center;
    padding: 25px 0px;
    margin-bottom: 20px;
    resize : none;
`;

interface TextAreaProps {
    changeFn : (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
}

export default function TextArea({changeFn} : TextAreaProps) {
    return (
        <Answer
            onChange={changeFn}
            placeholder="문제에 맞는 답을 적어 주세요 :)"
        />
    )
}
  • 상위 컴포넌트에서 onChange 함수를 Props로 받아 구현
  • Props 타입은 interface로 정리

useQuestionSet.ts

import { useSetRecoilState } from "recoil";
import { answerState, questionState } from "src/utils/storage";

export default function useQuestionSet() {

    const setQuestion = useSetRecoilState(questionState);
    const addQuestion = (e:React.ChangeEvent<HTMLInputElement>) => {
        setQuestion(e.currentTarget.value);
    }

    const setAnswer = useSetRecoilState(answerState); 
    const addAnswer = (e:React.ChangeEvent<HTMLTextAreaElement>) => {
        setAnswer(e.currentTarget.value);
    }
    
    return {
        addQuestion,
        addAnswer
    }
}
  • question, answer은 다른 컴포넌트에서 사용하는 state이기 때문에 recoil로 상태 관리
  • onChangeFunction 들을 리턴하는 커스텀 훅

QuestionNumber

QuestionTitle.tsx

import { SubTitle } from "src/components/commons/Commons"

interface QuestionNumberProps {
    questionNumber : number
}

export default function QuestionNumber({questionNumber} : QuestionNumberProps) : React.ReactElement {
    return (
        <SubTitle style={{color:'black', margin:'10px'}}>
            Question : {questionNumber} 
        </SubTitle>
    )
}
  • QuestionNumber를 상위 컴포넌트에서 Props로 내려받는 형태
  • Props는 QuestionNumberProps로 타입 정의

useLoadAllQuestion.tsx

import { useRecoilValue } from "recoil";
import { questionNum } from "src/utils/storage";

export default function useLoadAllQuestion() {
    const questionNumber = useRecoilValue(questionNum);
    return {
        questionNumber,
    }
}
  • questionNumber은 다른 컴포넌트들에서도 사용하기 때문에 recoil로 상태 관리
  • questionNumber를 리턴하는 custom hook

MovePageButton

MovePageButton.tsx

import { useNavigate } from "react-router-dom";
import { LargeButton } from "./CreateButton";

interface MovePageButtonProps {
    content : string;
    url : string;
}

export default function MovePageButton({content, url} : MovePageButtonProps) {
    const navigate = useNavigate();
    const movePage = () => {
        navigate(url)
    } 
    return(
        <LargeButton
            onClick={movePage}
        >
            {content}
        </LargeButton>
    )
} 
  • 버튼 내용, 이동하는 url을 상위 컴포넌트로 부터 Props로 받음
  • 버튼 내용, 이동하는 url을 interface로 타입 설정

Create Question Button

CreateButton.tsx

import styled from "styled-components"

export const LargeButton = styled.button`
    font-size: 20px;
    margin-top: 18px;
    width: 30%;
    border: 0px;
    height: 60px;
    border-radius: 10px;
    box-shadow: 0 4px 6px rgba(50, 50, 93, 0.15), 0 1px 3px rgba(0, 0, 0, 0.08);
    background-color: #3C73CF;
    color: white;
`

interface PrimaryLargeButtonProps {
    createQuesitonFn : (question : string, answer : string) => void;
    content : string
    question : string,
    answer : string 
}

export default function CreateButton({ createQuesitonFn, content, question, answer } : PrimaryLargeButtonProps ) {
    return (
        <LargeButton
            onClick={() => createQuesitonFn(question, answer)}
        >
            {content}  
        </LargeButton>
    )
}
  • createQuestionFn, content, createQuestionFn에 인자로 들어가는 question, answer를 상위 컴포넌트로 부터 Props로 받음

  • Props의 타입은 Interface로 정의

  • LargeButton은 다른 컴포넌트 (move page button, reset button)에서 사용하기 때문에 export

useCreate.ts

import { useRecoilState, useSetRecoilState } from "recoil";
import { answerState, questionNum, questionSet, questionState } from "src/utils/storage";

export default function useCreate() {

    const [question, setQuestion] = useRecoilState(questionState);
    const [answer, setAnswer] = useRecoilState(answerState);

    const setSet = useSetRecoilState(questionSet);
    const setQuestionNumber = useSetRecoilState(questionNum)

    const handleCreateQuestion = (question : string, answer : string) => {
        if(question === '' || answer === '') {
            alert("문제나 답을 다시 입력해주세요 :)");
            return
        }
        setSet((oldSet) => {
            const newSet = {id:Date.now(), question: question, answer: answer};
            return [newSet, ...oldSet];
        })
        
        setQuestionNumber((prevNum:number) => prevNum+1);
        setQuestion('');
        setAnswer('');
        window.location.reload();
    }

    return {
        handleCreateQuestion,
        question,
        answer
    }
}
  • CreateQuestionFn, question, answer는 recoil로 상태 관리
  • CreateQuestionFn, question, answer를 리턴하는 custom hook

Reset Button

ResetButton.tsx

import { LargeButton } from "./CreateButton";

interface ResetButtonProps {
    resetFn : () => void;
    content : string
}

export default function ResetButton({ resetFn, content } : ResetButtonProps ) {
    return (
        <LargeButton
            onClick={resetFn}
        >
            {content}  
        </LargeButton>
    )
}
  • resetFn, content를 상위 컴포넌트로 부터 Props로 받음
  • props는 interface로 정리

useReset.ts

import { useSetRecoilState } from "recoil";
import { correctNum, incorrectNum, questionNum, questionSet } from "src/utils/storage";

export default function useReset() {

    const setQuestionNumber = useSetRecoilState(questionNum);
    const correctQuestionsNum = useSetRecoilState(correctNum);
    const incorrectQuestionsNum = useSetRecoilState(incorrectNum);
    const setSet = useSetRecoilState(questionSet);

    const handleResetButton = () => {
        correctQuestionsNum(0);
        incorrectQuestionsNum(0);
        setQuestionNumber(0);
        setSet((prevQuestions) => {
            return prevQuestions = [];
        })
    }

    return {
        handleResetButton
    }
}
  • 맞는 개수, 틀린 개수, 문제개수, 문제-답 배열은 recoil로 상태 관리
  • resetFn를 반환하는 custom hook

0개의 댓글