프론트엔드 기본지식 Q&A 웹페이지 프로젝트

Lee·2022년 6월 20일
1

https://github.com/hautest/developer-q-and-a-

만들게 된 이유

현재까지 개발을 작은 프로젝트 만들기에만 급급하여서 기본적인 개념들의 부족함을 많이 느꼈다. 부족한 개념들을 공부하면서 그것들을 정리하여 볼 수 있는 웹을 만들고 싶었다.

기능구현

퀴즈페이지

퀴즈 형태로 정답과 답을 확인 할 수 있고 내가 모르는 Q&A는 따로 정리해서 볼 수 있게 체크 기능을 만든다.

Q&A한번에보기

퀴즈 형태가 아니라 Q&A들을 한번에 볼 수 있게 정리해놓은 페이지이다. 아까 퀴즈를 통해서 체크한 Q&A들을 따로 볼 수 있다.

사용한 기술들

next js, vanilla-extract

기존 프로젝트에서는 react와 styled-components를 사용해서 만들었으나 현재 내가 들어간 배달비 프로젝트에서 next js와 vanilla-extract를 사용해서 새로운 기술들을 익히기 위해서 사용했다.

코드 구현

_app.tsx

import GlobalLayout from "../components/GlobalLayout/GlobalLayout";
import "../styles/global.css";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <GlobalLayout>
      <Component {...pageProps} />
    </GlobalLayout>
  );
}

export default MyApp;

index.tsx

import { homeStyle, titleStyle } from "../styles/index.css";
import { themeClass } from "../styles/theme.css";
import Button from "../components/Button/Button";
import Link from "next/link";

function Home() {
  return (
    <div className={`${homeStyle} ${themeClass} `}>
      <div className={titleStyle}>DEVELOPER Q&A</div>
      <Link href="/quiz">
        <a>
          <Button size="lg">Quiz 시작하기</Button>
        </a>
      </Link>
      <Link href="/showAllQAndA">
        <a>
          <Button size="lg">Q&A한번에보기</Button>
        </a>
      </Link>
    </div>
  );
}

export default Home;

Quiz페이지와 Show all Q&A 페이지로 이동 할 수 있는 Link를 걸었다.

      <Link href="/quiz">
        <Button size="lg">Quiz 시작하기</Button>
      </Link>

처음에는 이런식으로 만들었는데 콘솔에 계속 warning이 나와서 a태그로 감싸해결을 했다.

Quiz.tsx

export default function Quiz() {
  const [questionNum, setQuestionNum] = useState(0);
  const [showAnswer, setShowAnswer] = useState(false);
  const [checked, setChecked] = useState(false);
  const maxQuestionNum = qAndAData.length - 1;
  const question = qAndAData[questionNum].question;
  const answer = qAndAData[questionNum].answer;
  const id = qAndAData[questionNum].id;

  useEffect(() => {
    if (typeof window !== "undefined") {
      const data = checkStorage.get();

      setChecked(!!data?.[id]);
    }
  }, [questionNum, id]);

  const handleToggle = () => {
    setShowAnswer((prev) => !prev);
  };
  const handleBefore = () => {
    if (questionNum === 0) {
      return;
    }
    setQuestionNum((prev) => prev - 1);
    setShowAnswer(false);
  };
  const handleNext = () => {
    if (questionNum === maxQuestionNum) {
      return;
    }
    setQuestionNum((prev) => prev + 1);
    setShowAnswer(false);
  };

  const handleCheck = () => {
    const localStorgeData = checkStorage.get();
    if (!!localStorgeData) {
      const find = localStorgeData[id];

      if (find) {
        checkStorage.remove(id);
      } else {
        checkStorage.append(id, { question, answer });
      }
    } else {
      checkStorage.set({ [id]: { question, answer } });
    }
    setChecked((prev) => !prev);
  };

  return (
    <div className={quizBox}>
      <article className={qAndACard}>
        {!showAnswer && <p>{question}</p>}
        {!!showAnswer && <p>{answer}</p>}
      </article>
      <div className={buttonBox}>
        <Button size="md" onClick={handleToggle}>
          {showAnswer ? "문제보기" : "정답보기"}
        </Button>
        <Button size="md" onClick={handleCheck}>
          {checked ? "체크해제하기" : "체크하기"}
        </Button>
        <Button size="md" onClick={handleBefore}>
          이전
        </Button>
        <Button size="md" onClick={handleNext}>
          다음
        </Button>
      </div>
    </div>
  );
}

Quiz 페이지는 이런식으로 만들었다. 간단한 프로젝트라 서버가 굳이 필요없다고 느껴서 데이터들을 서버가 아닌 로컬스토리지에 저장했다. 로컬스토리지를 작동시키는 함수들을 직접 입력하니 코드의 가독성이 매우 떨어져서 로컬스토리지utils를 따로 만들어 로컬스토리지를 관리했다.

createLocalStorage.tsx

export const createLocalStorage = <T>(key: string) => {
  const set = (data: T) => {
    localStorage.setItem(key, JSON.stringify(data));
  };
  const get = () => {
    const value = localStorage.getItem(key);
    return value ? (JSON.parse(value) as T) : null;
  };
  return {
    get,
    set,
    append<K extends keyof T>(key: K, value: T[K]) {
      const data = get();
      if (!value) throw new Error("value is null");
      if (data) {
        set({ ...data, [key]: value } as unknown as T);
      } else {
        set({ [key]: value } as unknown as T);
      }
    },
    remove<K extends keyof T>(key: K) {
      const data = get();
      const { [key]: _, ...rest } = data;
      set(rest as T);
    },
  };
};

checkStorage.tsx

import { createLocalStorage } from "./createLocalStorage";

interface LocalStorageInterface {
  question: string;
  answer: string;
}

export const checkStorage =
  createLocalStorage<Record<number, LocalStorageInterface>>("check");

이런식으로 localStorage관리를 따로 했다.

showAllQAndA.tsx

interface ListProps<T> {
  list: T[];
  renderItem: (value: T, index: number) => ReactNode;
  className?: string;
}

function List<T>({ list, renderItem, className }: ListProps<T>) {
  return (
    <ul className={`${baseListStyle} ${className}`}>{list.map(renderItem)}</ul>
  );
}

export default function ShowAllQAndA() {
  const [toggle, setToggle] = useState(false);

  const hadleToggle = () => {
    setToggle((prev) => !prev);
  };

  return (
    <main className={showAllQAndABox}>
      <Button onClick={hadleToggle} size="lg">
        {toggle ? "전체보기" : "체크한것만 보기"}
      </Button>
      <List
        className={flexCenter}
        list={toggle ? Object.values(checkStorage.get() ?? {}) : qAndAData}
        renderItem={({ question, answer }) => (
          <li className={qAndABox} key={answer}>
            <div>{question}</div>
            <hr className={lineColor} />
            <div>{answer}</div>
          </li>
        )}
      />
    </main>
  );
}

toggle을 따로 만들어서 전체 q&a랑 체크한것들만 따로 볼 수 있게 관리하였다.

기본 Q&A관련 데이터들은 현재는 임시의 데이터들로 되어 있는데 이제부터 하루에 하나씩 공부를 하면서 채워나가야한다.

어느정도 데이터가 쌓이면 배포를 해서 사용할 생각이며 나중에 새로운 Q&A추가, 로그인 기능도 넣을까 고민중인데 만약 한다면 notion을 이용해서 DB관리를 할까 생각중이다.

피드백해 주실 것들 있으시면 피드백해 주시면 감사하겠습니다 :)

profile
프론트엔드 개발자

0개의 댓글