본문 바로가기
Js&React 실습/React Prac

[React/pre-project] stackoverflow 클론 코딩

by sweesweet 2022. 9. 10.

팀 프로젝트

프론트2/ 백3

⏲️ 개발 기간

22.08.23 ~22.09.06(계획포함)

22.08.25 ~ 22.09.06 (실제 개발 기간)

 

사용한 라이브러리(프레임워크)

React v18(create-react-app), styled-component, zustand, react-router, toast-ui, axios

 

01234

 

 

이번 프리프로젝트에서는 stackoverflow 클론코딩을 하게 되었다. 전체를 클론코딩 하기 보다는, 우리가 구현할 수 있는 부분만 구현하기로 했고, 과감히 메인 화면은 구현하지 않도록 했다.

 

 

 

 

내가 맡은 부분

반응형 웹 구현
회원 가입, 로그아웃
질문 리스트 조회(2개 로그인을 했을 때/ 안했을 때), 글 검색 조회, 질문 상세 조회
- pagination/sort 구현
질문/ 답변-  삭제
질문/ 답변의 댓글 - 등록/수정/삭제
질문/ 답변 추천과 비추천 기능
답변 채택 기능
컴포넌트 기준
Nav,Answer,Question,ListItem,SortBar,Pagination,LikeRate,NoResult
페이지 기준
Signup,DetailQuestion,MainLogin,MainLogout,SearchResult

페이지 갯수는 얼마 되지 않았다.

[ 회원가입/ 질문조회/ 글 검색조회/ 질문상세조회]

다만....페이지네이션, sort ,추천비추천 등등 짜잘짜잘한 기능들이 모여있어서 혼자 구현하는데 좀 벅찼던것같다...


구현 중에 

겪었던 문제 1

Comment 컴포넌트를 재활용하면서(답변/질문 둘 다 사용) 로그인x/로그인o 때의 상황을 고려하지 못한 점. 뒤늦게 예외 처리를 하게 됐는데, 조건부 렌더링을 실수 한 것 같다/ 우선 유저일때/ 유저가 아닐때를 고려하고 유저일때 글 쓴사람과 이름이 같다면 답변 채택 혹은 댓글의 수정 삭제가 보이게 했었어야했다...

코멘트를 답변 /질문을 동시에 사용하기때문에 => 우선 status에따라 랜더링하고=> 유저가 로긴했을때/안했을때=> 했다면 유저이름이 같을때 이렇게 조건부 렌더링을...해야했었던 것 같다! 맘이 좀 급했고 로컬에 없을때  유저네임을 'x' 이런식으로...ㅎ....했다(문제 있음)

 

해당 Comment 코드보기 👇(되려나>)

더보기

어후 어지러운 코드~

const Comment = ({ status, data, id, originData, setData }) => {
  const [isOpen, setIsOpen] = useState(false);
  const content = useRef();
  const username = window.localStorage.getItem('USER_INFO')
    ? JSON.parse(window.localStorage.getItem('USER_INFO')).username
    : 'x';

  const commentId =
    status === 'questions' ? data.questionCommentId : data.answerCommentId;
  const handleEdit = async (e) => {
    e.preventDefault();
    if (window.confirm('댓글을 수정하시겠습니까?')) {
      if (content.current.value === '') {
        return;// 아쉬운 부분 수정하시겠습니까 안에 넣는게 아니구 다른 if로했다면 더 좋았을듯
      }
      const patchData =
        status === 'questions'
          ? {
              memberId: 1,
              questionCommentId: data.questionCommentId,
              question: {
                questionId: id,
              },
              questionCommentContent: content.current.value,
            }
          : {
              memberId: 1,
              answerCommentId: data.answerCommentId,
              answer: {
                answerId: id,
              },
              answerCommentContent: content.current.value,
            };

      await reIssue
        .patch(
          `/${status}/${id}/comments/${
            status === 'questions'
              ? data.questionCommentId
              : data.answerCommentId
          }`,
          patchData
        )
        .then(({ data }) => {
          if (status === 'questions') {
            setData({
              ...originData,
              questionComments: originData.questionComments.map((el) => {
                if (el.questionCommentId === data.data.questionCommentId) {
                  return data.data;
                }
                return el;
              }),
            });
            alert('댓글이 수정되었습니다');
            setIsOpen(false);
          } else {
            setData({
              ...originData,
              answers: originData.answers.map((el) => {
                if (el.answerId === id) {
                  return {
                    ...el,
                    answerComments: el.answerComments.map((el) => {
                      if (el.answerCommentId == data.data.answerCommentId) {
                        return data.data;
                      }
                      return el;
                    }),
                  };
                } else {
                  return el;
                }
              }),
            });
            alert('댓글이 수정되었습니다');
            setIsOpen(false);
          }
        })
        .catch(() => {
          alert('댓글수정에 실패하였습니다');
        });
    } else {
      return;
    }
  };
  const handleDelete = () => {
    if (window.confirm('댓글을 삭제하시겠습니까?')) {
      reIssue
        .delete(
          `/${status}/${id}/comments/${
            status === 'questions'
              ? data.questionCommentId
              : data.answerCommentId
          }`
        )
        .then(() => {
          if (status === 'questions') {
            setData({
              ...originData,
              questionComments: originData.questionComments.filter(
                (el) => el.questionCommentId !== commentId
              ),
            });
          } else {
            setData({
              ...originData,
              answers: originData.answers.map((el) => {
                if (el.answerId === id) {
                  return {
                    ...el,
                    answerComments: el.answerComments.filter(
                      (el) => el.answerCommentId !== commentId
                    ),
                  };
                } else {
                  return el;
                }
              }),
            });
          }

          alert('댓글이 삭제되었습니다');
        })
        .catch((err) => {
          alert('댓글 삭제 실패하였습니다');
          console.log(err);
        });
    } else {
      return;
    }
  };
  return (
    <CommentLi>
      {isOpen === false ? (
        <>
          {status === 'questions' ? (
            <span>{data.questionCommentContent}</span>
          ) : (
            <span>{data.answerCommentContent}</span>
          )}
          {status === 'questions' ? (
            <span className="author">{data.questionCommentUsername}</span>
          ) : (
            <span className="author">{data.answerCommentUsername}</span>
          )}
          {username ===
          (status === 'questions'
            ? data.questionCommentUsername
            : data.answerCommentUsername) ? (
            <div className="editNdelete">
              <button
                className="editBtn"
                onClick={() => setIsOpen(!isOpen)}
              ></button>
              <button className="deleteBtn" onClick={handleDelete}></button>
            </div>
          ) : (
            <></>
          )}
        </>
      ) : (
        <>
          <WriteComment>
            <button className="close" onClick={() => setIsOpen(!isOpen)}>
              x
            </button>
            <textarea
              ref={content}
              className="editComment"
              defaultValue={
                status === 'questions'
                  ? data.questionCommentContent
                  : data.answerCommentContent
              }
            />
            <button onClick={handleEdit} className="submitComment">
              Edit
            </button>
          </WriteComment>
        </>
      )}
    </CommentLi>
  );
};

export default Comment;

겪었던 문제 2

백엔드가 로컬에 서버를 만들어서 덕분에 발견했던 문제인데, 통신 속도가 느리니까 화면이 렌더링되는 시간이 통신 속도보다 더 빨라서 reading undefined 오류가 발생했다. 껍데기라도 보여야했는데 reading undefined 오류가 발생하면서 빈화면이 나오게 됐다. 그래서 useEffect로 렌더링시 데이터를 받는 유형일 경우 useState로 pending의 유무를 설정해서 로딩화면을 띄우도록 했다. 모든 사람이 빠른 컴퓨터를 사용하는 게 아님을 항상 인지하며 페이지를 만들어야겠다고 생각했다.

No result도 통신에 실패했을 때 유저에게 알리고자 만들었다.status를 props로 내려서 조건을 나눠서 오류가 발생하는 원인을 화면에 나타나게 했다.

해당 noresult 사용한 코드보기 👇(되려나>2)

더보기
const MainLogout = () => {
  // const [data, setData] = useState([]);
  // const [pageInfo, setPageInfo] = useState({});
  const [ispending, setIsPending] = useState(true);
  const navigate = useNavigate();
  const [noResult, setNoResult] = useState({
    status: 'data',
    keyword: 'no data',
  });
  const { setPagination, data, setData, setSort, setPageInfo, pageInfo } =
    useSortStore((state) => state);
  useEffect(() => {
    defAxios
      .get(`/questions?page=1&sort=newest&filters=`)
      .then(({ data }) => {
        setData(data.data !== undefined ? data.data : []);
        setPageInfo(data.pageInfo);
        setIsPending(false);
      })
      .catch((err) => {
        setData([]);
        setTimeout(() => {
          setNoResult({ status: 'httpErr', keyword: err.response.status });
          setIsPending(false);
        }, 200);
        console.log(err);
      });
    return () => {
      setPagination(1);
      setSort('newest');
      setData([]);
      setPageInfo({});
    };
  }, []);

  return (
    <Div>
      <MainContainer>
        <div className="pageDesc">
          <h2>All Questions</h2>
          <button onClick={() => navigate('/write')}>Ask Question</button>
        </div>
        <div className="totalNbtns">
          <div className="totalQuestion">
            {pageInfo.totalElements !== undefined && pageInfo.totalElements}{' '}
            questions
          </div>
          <SortBtnBar
            setData={setData}
            setIsPending={setIsPending}
            setNoResult={setNoResult}
          />
        </div>
        {ispending === true ? (
          <>
            <Loading />
          </>
        ) : (
          <>
            {data.length !== 0 ? (
              <>
                <ul>
                  {data.map((el) => (
                    <QuestItem el={el} key={el.questionId} />
                  ))}
                </ul>
                <Pagination
                  setIsPending={setIsPending}
                  setNoResult={setNoResult}
                  status={'question'}
                />
              </>
            ) : (
              <>
                <NoResult keyword={noResult.keyword} status={noResult.status} />
              </>
            )}
          </>
        )}
      </MainContainer>
      <RightSideBar />
    </Div>
  );
};

 

겪었던 문제 3

백엔드에서 토큰의 제한시간을 30분으로 걸었고, 그 토큰이 만료되면 로컬스토리지에있는 엑세스토큰과 리프레시 토큰 두개를 /members/reissue로 다시 보내주면 다시 토큰이 발급되고 그 토큰으로 다시 해당 통신을 다시 걸게 해달라고했다. 우선 프엔 둘이서 따로 그 부분에 대해서 코드를 짜기로 했었는데 짜면서 여러번의 위기가 오게된다..ㅋㅋ

1. axios interceptor를 이용해서(폭풍구글링) 코드를 짰다. 다른 분들은 axios.create를 이용해서 해당 변수를 만들고, reissue때도 그 변수를 사용했다-> 폭풍 무한루프 발생. 토큰은 계속 만료되어있구 리이슈에 토큰을 발급하기도 전에 또 요청을 하기때문에 무한 루프가 발생한다...

2. 무한 루프를 고쳤더니 안됐다. 토큰이 발급이 되긴 하지만 저장이 안되면서 토큰이 undefined로 저장이 되고 401(unauthorized) 발생. 그때 다른사람의 코드를 따라 친 것과 마찬가지였기 때문에 const data = await axios.post 이런식으로 data에하고, 그 데이터가 있다면 토큰을 저장하는 방식으로 했어서 그런가, 되지 않았다. 그래서 if data부분을 삭제하고 response가 있다면 데이터를 받아서 변경하는식으로 해당코드를 then안에 넣었더니 토큰을 발급받고 저장할 수 있었다.(통신이 빨라도 undefined오류가 발생하고 그럴려나?)

- 사소한 프록시 설정

cra에서 개발 때 사용했던 proxy 기능때문에 까먹구....baseUrl설정을 놓치고 업로드를 하게됐다. 네트워크 탭을보고 주소가 프론트주소에 해당되는걸 확인하기전까진 정말 억울하고 그랬는데(발급되던게 안되니까 코드 멱살잡고싶었음) 확인하고 나니 코드는 잘못 없고 짠 사람이 잘못이 많다는걸 또 느끼게 된 하루였다

+ 뭐 아쉬웠던 점은 프엔끼리 공통으로 reset.css 혹은 레이아웃 구조같은걸 생각하고 했어야했는데 ...(말줄임)

 

겪었던 문제가 또 생각이 난다면 와서 찾아와서 추가할거다!!!!

메인때는 게시판을 안하기로 해서 진짜 또 다른 도전이 될 것 같다. 진짜 힘내자!!

 

++아쉬웠던점 추가!!

ㅈㅊ님의 프엔 배포를 봤는데 pagination 부분에 총 페이지 갯수가 10이 넘어가면 ...next처리해놓은걸 볼 수 있었다.

넘 바빠서 까먹었는데 나도 글케할걸... 그냥 page갯수 받으면 그거로 div생성할 생각만했지, 버튼은 생각을 못했다.. 아숩다 아수워

'Js&React 실습 > React Prac' 카테고리의 다른 글

[React/main-project] 선인장 키우기  (0) 2022.10.11
[React] Colorful Diary  (0) 2022.09.03