goodbye 2021

dana·2022년 1월 20일
8

토이프로젝트

목록 보기
10/17

프로젝트 소개

2021년을 회고하고 공유하기 위한 웹앱 제작
https://www.goodbye2021.site/

사용 스택

  • React
  • javascript
  • styled-components
  • zustand

실행 예시

✨ new

figma


그동안 피그마 문서를 보기만 했지 직접 디자인을 위해 사용해보는건 처음이었다. 아직은 어색해 기본적인 도형을 가지고 디자인을 구현했는데, 다음에는 피그마를 이용해 모듈화 작업을 도전해보고 싶다.

zustand

전역관리를 위해 zustand를 처음 사용해보았다. 규리님네 회사에서 사용하는데 굉장히 편하다고 해서 처음 사용해보았는데 정말 간편해서 다음에도 사용해보면 좋을 듯하다.

우선, npm install zustand 혹은 yarn add zustand를 이용해 설치해준다.
전역관리를 실행할 컴포넌트를 따로 만들어준 뒤, 설치한 zustand를 import 해준다.

import create from 'zustand'

const useStore = create(set => ({
  // bears 라는 변수 선언
  bears: 0,
  // bears 수 증가
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  // bears 0으로 초기화
  removeAllBears: () => set({ bears: 0 })
}))

예시에 있는 코드인데, 풀어서 설명을 해보자면 set을 이용해 우리가 useState를 이용하듯 값을 입력해주면 된다.
useStore 안에 관리하고자 하는 변수를 두고, 그 변수를 변경할 (건드릴) 함수를 선언해준다. 이때 set을 이용해 값을 변경해주는데, 인자값으로 넘겨주는 state는 useStore 객체를 의미한다고 보면 된다.

이를 이용해 규리님이 사용자 이름을 받아 사용하는 부분을 보고 더 쉽게 이해할 수 있었다.

const useStore = create((set) => ({
  name: "",
  setName: (state) =>
    set(() => ({
      ...useStore,
      name: state,
    })),
}))

set은 기존 객체를 새로운 값으로 덮어씌우기 때문에 object를 value로 주는 경우, 주의해야한다.

React-router-dom

이 부분은 내가 구현하지 않았지만, React-router를 처음 프로젝트에 적용해봤는데 규리님의 코드가 깔끔해 다음 프로젝트에 사용하는 경우를 위해 적어두려고 한다. 현재 리액트 라우터가 v6으로 업데이트 되었기 때문에 이에 관련해서 새로운 글을 적어보도록 하겠다...(이렇게 또 할 일 추가요)

우선 react-router-dom 을 설치한 뒤, index.js(최상단파일)에 import { BrowserRouter, Route, Routes } from "react-router-dom" 을 가져온다.

ReactDOM.render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />} />
      <Route path="answer" element={<Answer />} />
      <Route path="question" element={<Question />} />
      <Route path="result" element={<Result />} />
    </Routes>
  </BrowserRouter>,
  document.getElementById("root")
)

<Route path="/" element={<App />} /> 에서 보이는 것처럼 <BrowserRouter>로 감싸준 뒤, 원하는 url path값을 작성해준다. path = "answer" 이라면 www.example.com이라는 도메인이 있을 때, www.example.com/answer에 접근시 작성한 element 컴포넌트로 이동하게 된다. 쏘 심플

라우터가 적용된 컴포넌트 내에서 다른 특정 컴포넌트로 이동하고 싶다면 toLink attribute를 이용해 이동할 수 있다.
<Button toLink={"/question"} children={"2021 기록하기"} /> -> 버튼 클릭시 question으로 이동. 이 때 이동되는 경로는 절대경로이다.

meta tags

  <meta name="viewport" content="width=device-width, initial-scale=1 maximum-scale=1.0, user-scalable=no" />
  <meta name="theme-color" content="#000000" />
  <meta name="description" content="welcome 2022" />
  <meta property="og:image"
    content="https://user-images.githubusercontent.com/47337588/147823654-89c99fcc-a9ee-43ff-8889-ee7aca1bb561.png" />

user-scalable=no

input태그를 넣게되면 오른쪽 하단에 유저가 크기를 자유자재로 조절할 수 있는 버튼(?)이 생긴다. 이를 허용하지 않기위해 meta태그의 viewport 속성에 user-scalable = no 를 넣어주면 유저가 임의로 크기를 변경하는 것을 막을 수 있다.

theme-color

theme-color를 이용해 웹의 해당 부분의 색상을 지정해 줄 수 있다.

하지만 대부분의 브라우저에서 지원하지 않는다.

property="og:image"

메타태그를 이용해 다음과 같이 공유시 썸네일을 지정해주었다.

image 이외에도 다양한 속성들을 나타낼 수 있다. [공식문서]를 참고해도 좋고, 다음에 SEO를 다룰 때 한번 더 언급하도록 하겠다.

🔥 hard

zustand를 이용해 object in object 다루기

앞서 언급했다싶이 set은 기존 객체를 새로운 객체로 덮어씌우기 때문에 value값이 object in object로 이뤄진 경우, 깊은 복사를 하는 과정이 필요하다. 게다가 내부의 값을 꺼내오고, 특정값을 변경하는 부분에서 많은 고민을 했다. 이와 비슷한 고민을 futurama 프로젝트에서도 했었는데, 그 때는 결론적으로 답만 저장하면 됐기때문에 spread 문법을 이용해 쉽게 구현할 수 있었다.

하지만 지금은 사용자가 선택한 질문과 답변을 모두 저장해야하기 때문에, 배열안에 객체를 넣어 사용하는게 불가피했다. 값을 변경하기 위해선 set 함수 안에 변경된 새 객체를 넣어야하는데, 객체 안의 객체의 값을 변경한 뒤에 값을 넣어야하는 이상한 재귀에 빠지게 되었다. 그래서 찾아본 결과, immer를 이용해 쉽게 구현하는 방법을 찾아냈다.

store.js

우리에게 필요한 질문의 갯수는 정해져있었기 때문에 템플릿을 미리 만들어두고, 값을 업데이트하는 방식을 이용해서 다음의 useQuest라는 전역 객체를 만들어주었다.

const useQuest = create((set) => ({
  qna: [
    {
      id: 0,
      question: "",
      answer: "",
    },
    {
      id: 1,
      question: "",
      answer: "",
    },
    {
      id: 2,
      question: "",
      answer: "",
    },
    {
      id: 3,
      question: "",
      answer: "",
    },
    {
      id: 4,
      question: "",
      answer: "",
    },
  ],

  editQuestion: (q) => {
    set(
      produce((draft) => {
        const qu = draft.qna.find((el) => el.id === q.id)
        qu.question = q.question
      })
    )
  },
  editAnswer: (a) => {
    set(
      produce((draft) => {
        const an = draft.qna.find((el) => el.id === a.id)
        an.answer = a.answer
      })
    )
  },
}))

question.jsx

사용자가 클릭한 질문은 계속 변경되고, 최종적으로 업데이트 된 객체를 넘겨야하기 때문에 우선 question에 질문들을 저장할 array를 만들어주었다. false를 기본값으로 가진 질문 갯수만큼의 배열을 만들어 준 뒤, 체크되면 해당 질문의 인덱스과 같은 인덱스의 값이 true로 바뀌도록 했다.

const [checklist, setChecklist] = useState(new Array(dataLength).fill(false))

  const handleCheck = (idx) => {
    if (!checklist[idx] && [...checklist].filter((e) => e).length >= 5) {
      alert("키워드는 5개까지만 입력해주세요.")
    } else {
      checklist[idx] = !checklist[idx]
      setChecklist([...checklist])
    }
  }

true를 체크하면서 갯수도 체크해 5개보다 적거나 넘치는 경우를 방지했다.
5개를 체크한 뒤 다음으로 넘어가기 위해 버튼을 누르면 최종적으로 array의 true값을 확인하면서, 인덱스에 맞춰 질문을 useQuest에 저장해주도록 만들었다.

const handleClick = () => {
   if ([...checklist].filter((e) => e).length !== 5) {
     alert("키워드를 5개 선택해주세요.")
   } else {
     setIsActive(true)
     let selected = []
     checklist.forEach((e, idx) => e && selected.push(QuestionData[idx]))
     selected.forEach((e, idx) => editQuestion({ id: idx, question: e }))
   }
 }

회고글을 작성하면서 해당 코드의 결함을 발견했다..
만약 답변까지 작성하고 결과창으로 넘어간 뒤, 다시 뒤로 가기를 해 질문을 재선택하고 답변을 입력하지 않은채로 다음으로 넘어가면 질문은 바뀌고 대답은 그대로 유지된다.

다음 오류를 해결하기 위해 대답을 입력하지 않는 경우 넘어가지 못하도록 조건을 걸어야겠다.

profile
PRE-FE에서 PRO-FE로🚀🪐!

0개의 댓글