모듈화의 필요성
이 프로젝트는 영화에 관한 25개의 퀴즈를 풀 수 있는 사이트인데, 코드를 짜면서 문제를 여러 번 수정하거나 삭제하는 과정을 거쳤다. 그런데 중간중간 문제를 새로 추가하거나 순서를 바꾸는 일, 해당 부분을 찾아 블록 단위로 코드를 옮기거나 변경하는 일이 매우 불편하게 느껴졌다. 결과적으로 프로젝트를 완성한 다음, 모든 문제를 별도의 라이브러리에 모듈 형태로 저장하는 리팩토링 작업을 했다.
모듈 활용
각각의 문제를 객체 형태로 만들고 난 뒤, question, answer, options를 키로 설정했다. 또 문제의 type과 caption, reference라는 특수 항목을 만들어 문제가 주관식인 경우, 그리고 문제를 다 풀고 나서 제공되는 해설지에서도 이 모듈을 효과적으로 활용할 수 있도록 했다.
export const data = [
{
type: "multiple-choice",
question: "다음 중 <헤어질 결심>(2022, 박찬욱)에 등장하지 않는 음식은?",
answer: 2,
options: ["초밥", "볶음밥", "파스타", "석류"],
caption:
"해준이 서래를 심문할 때 시마스시에서 특초밥 세트를 주문해 처음으로 함께 식사를 한다. 이포로 이사한 해준은 거실에서 석류를 손질하고, 나중에 서래에게 중국식 볶음밥을 요리해준다.",
},
{
type: "multiple-choice",
question: "다음 중 <벌새>(2018, 김보라)에 등장하는 대사가 아닌 것은?",
answer: 2,
options: [
"제 삶도 언젠가 빛이 날까요?",
"언니, 그건 지난 학기잖아요.",
"더 나아지기 위해 우리는 기꺼이 더 나빠졌다. 그게 우리의 최선이었다.",
"우리는 늘 누군가를 만나 무언가를 나눈다는 것, 세상은 참 신기하고 아름답다.",
],
caption: "<최선의 삶>(2019, 이우정)에 등장하는 대사이다.",
reference: "https://youtu.be/WjJ6pdVeOAg?t=88",
},
{
type: "multiple-choice",
question: "다음 중 소설가 무라카미 하루키의 소설을 바탕으로 만든 영화가 아닌 것은?",
answer: 3,
options: [
"드라이브 마이 카(2021, 하마구치 류스케)",
"버닝(2018, 이창동)",
"토니 타키타니(2004, 이치카와 준)",
"환상의 빛(1995, 고레에다 히로카즈)",
],
caption: "참고로 원작 소설의 제목은 <헛간을 태우다>이다.",
reference: "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=49852122&start=slayer",
},
// ...
];
Question 컴포넌트에서는 위 모듈에 map 반복문을 돌려 화면에 문제를 표시하고, 특수 항목이 포함된 문제는 별도의 인터페이스와 기능을 넣었다. 이렇게 문제를 모듈화하고 나니 코드의 길이가 기존 상태보다 5배 정도 줄었다. 문제의 수가 많을수록 차이는 더욱 컸을 것이다. 앞으로는 최소 3회 이상 반복되는 코드가 있다면 모듈화가 필요한 상태라 생각하고 그때그때 클린 코드를 만드는 습관을 들여야겠다.
export function Question({ page, answer, setAnswer }: QuestionProps) {
// ...
const Options = () => {
return data[page - 1].options?.map((option, index) => {
return (
<React.Fragment key={index}>
<div
// ...
>
{`${index + 1}) ${option}`}
</div>
</React.Fragment>
);
});
};
return (
<div className={styles["question-container"]}>
<div className={styles["question"]}>
{`${[page]}. `}
// ...
</div>
{data[page - 1].type === "multiple-choice" ? (
data[page - 1].type2 === "image" ? (
<React.Fragment>
<div className={styles["image-container"]}>
<Image
className={styles["image"]}
src={`/cinephile/${data[page - 1].title}.webp`}
alt={`${data[page - 1].title}`}
width={window.innerWidth > 450 ? "420" : "240"}
height={window.innerWidth > 450 ? "290" : "160"}
/>
</div>
<Options />
</React.Fragment>
) : data[page - 1].title === "chungking-express" ? (
<React.Fragment>
<div className={styles["chungking-express"]}>📞 🍍 🕒 😎</div>
<div className={styles["chungking-express"]}>👮♂️ 💌 🔑 🛫</div>
<Options />
</React.Fragment>
) : (
<Options />
)
) : data[page - 1].type === "short-answer" ? (
<div className={styles["short-answer-container"]}>
{data[page - 1].paragraph?.split(String(data[page - 1].answer)).map((text, index) => {
return index === 0 ? (
<React.Fragment key={index}>
{text}
<input
className={styles["short-answer-input"]}
onChange={e => {
setAnswer(e.target.value);
}}
/>
</React.Fragment>
) : (
<React.Fragment key={index}>{text}</React.Fragment>
);
})}
</div>
) : null}
</div>
);
}
반응형