Ming's develop story

실전프로젝트 1주차 진행과정 - 1 본문

스파르타코딩클럽 - 항해99/실전 프로젝트 - TIL && WIL

실전프로젝트 1주차 진행과정 - 1

Ming 2021. 12. 24. 04:03

발제가 끝난 뒤에 실전프로젝트를 같이 진행할 팀이 랜덤으로 꾸려졌고, 첫번째 회의에서 나는 이왕 하는거 더 바쁘고 더 열심히 해보자는 마인드로 팀장에 자원했다.

 

회의 끝에 결정된 주제는 물물교환에 관한 커뮤니티이다. 

몇일 뒤에 정해진거긴 하지만 우리 프로젝트 이름은 '핑퐁'이다.

 

와이어 프레임을 디자이너 분들의 도움과 다른 프론트엔드 팀원들과 짜보았는데, 

이런 형태로 웹에서 모바일이 있는것과 같은 느낌으로 만들어 보기로 했다.

핑퐁 와이어프레임

처음에 어느정도 대충 틀을 잡아논 뒤에 백엔드분들과 같이 API 명세를 작성하는데 막상 작성 할수록, 또 와이어프레임을 다듬을수록 필요한 api와 페이지가 하나 둘 씩 늘어났다...

 

상대적으로 다른 조들에 비해 과정이 순조롭게 진행 되었는데 아마 웬만하면 예스를 외쳐주던 팀원들 또, 그들의 적극적인 참여 덕분이 아니었나 싶다.

 

화요일 아침 프론트 엔드 3명이 따로 모여 어떤식으로 진행할지 회의를 하고 기본적인 개발 환경을 셋팅하고 내가 github도 Organizations로 프론트, 백이 각각의 레파지토리에서 협업할 수 있도록 만들어 두었다.

 

일단 우리에게 메인 기능중 하나라고 하자면 websocket을 통한 실시간 채팅 기능인데 이는 다음주부터 시작하기로 했기때문에 그 전까지의 6개 페이지를 먼저 제작하기로 결정이 났다. 이전까지의 과정에서 수없이 만들어보았던 회원가입과 로그인 페이지기에 이번엔 또 자주 해보지 못했던 페이지들에 도전을 하고 싶었다. 그래서 앞선 6개 페이지 중 내가 써보지 않은 기술을 포함하고 있는 메인과 글 작성 페이지를 맡아서 하기로 했다.

 

여기서 중요한 기능으로는 swiper(슬라이드)와 부분 무한스크롤, 해시태그 추가, 여러장의 이미지 업로드 등이 있었다.

 

메인 페이지
게시물 작성 페이지

 일단 지금까지의 진행사항은 무한스크롤, 여러장의 이미지 업로드 빼고는 구현을 완료하였다.

또, 태그를 추가하고 input의 value값이 초기화 되도록 코드를 구현하였는데 작동하질 않아서 해결 방법을 찾아봐야 한다.

 

메인페이지

1.

위의 주황색버튼 부분에는 품목 카테고리의 아이콘이 들어갈 부분이다.

슬라이드를 구현할 때 swiper라는 패키지를 사용하였는데, docs의 가이드에 따라 했지만 오류가 났었다.

다른 사람들이 구현했던것을 참고 해가며 결국 구현 해냈는데 막상 하고나니 너무 쉬운 부분이었다.

import React from "react";
import styled from "styled-components";
import { Swiper, SwiperSlide } from "swiper/react";
import SwiperCore, { Pagination, Autoplay } from "swiper";

// style
import "swiper/css";
import "swiper/css/pagination";

SwiperCore.use([Pagination, Autoplay]);

const MainCategory = () => {
  return (
    <React.Fragment>
      <Slider>
        <Swiper
          className="CateBtn-Container"
          spaceBetween={15}
          slidesPerView={5}
          pagination={{ clickable: true }}
          autoplay={{ delay: 2000 }}
        >
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
          <SwiperSlide>
            <CateBtn />
          </SwiperSlide>
        </Swiper>
      </Slider>
    </React.Fragment>
  );
};

const Slider = styled.div`
  height: 80px;
  margin: 15px;
  margin-top: 40px;
  display: flex;
  .swiper-pagination-bullet-active {
    background-color: #ff8a3d !important;
    width: 16px !important;
    border-radius: 4px !important;
  }
`;

const CateBtn = styled.img`
  width: 50px;
  height: 50px;
  border-radius: 50px;
  background-color: var(--point-color);
`;

export default MainCategory;

여기서 겪었던 문제점은 style을 import 하는 부분에서 인터넷 상에 대부분 나와있는 swiper에 대한 포스팅들은 경로가 css 가 아닌 scss 또는 min/css 등 나와는 맞지 않는 경로였다. 똑같은 방법으로 설치한 패키지인데 왜 이런 차이점이 있는지는 명확하게 밝혀내지 못했지만 구글링해서 찾은것을 다 시도해 보다보니 나에게 맞는 방법을 찾게되었다.

 

대부분 docs에 나와있는 내용과 비슷해 딱히 짚고 넘어갈 것은 없다고 느껴지지만 속성들에 대해 좀 다뤄보자면 pagination은 슬라이드 밑에 ... 과 같이 갯수를 보여주는? 속성이다. 또한 clickable: true를 줘서 클릭으로 넘어갈 수 있기도 하다.

autoplay는 자동으로 슬라이드가 넘어갈 수 있도록 해주는 속성이고,

slidesPerView는 한번에 보일 슬라이드의 컨텐츠 갯수이고,

spaceBetween은 컨텐츠간의 간격으로 보면 된다.

그리고 나는 추가로 개발자 도구를 사용해 보여지는 컨텐츠에 해당하는 pagination의 className을 찾아 active가 되었을땐 조금 더 긴 원형으로 보이도록 css를 설정해주었다. ( 이 부분에서 !important를 사용했는데 이는 css를 우선적으로 적용한다는 뜻이다. )

 

2.

select box를 만들때 React에서는 또 쉽게 만들수 있도록 라이브러리가 있지만 나는 그냥 하드코딩으로 만들어 보기로 했다. 

react-select 패키지

-> 코드가 짧아져 가독성이 좋지만, style을 세세하게 주려고 하면 약간 힘들 수 있다.
div로 만드는 select 컴포넌트

-> 코드가 길어져 가독성이 좋지 못할 수 있지만, style을 세세히 줄 수 있따.

import React from "react";
import styled from "styled-components";
import { GrMap } from "react-icons/gr";

const LocationSelectBox = () => {
  const option = [
    { value: "구", name: "구" },
    { value: "강남구", name: "강남구" },
    { value: "서초구", name: "서초구" },
    { value: "동대문구", name: "동대문구" },
  ];

  return (
    <React.Fragment>
      <SelectBoxWrapper>
        <LocationSelect>
          {option.map((option) => (
            <option
              key={option.value}
              value={option.value}
              hidden={option.value === "구" ? true : false}
            >
              {option.name}
            </option>
          ))}
        </LocationSelect>
        {/* <GrMap
          style={{
            marginLeft: "-20px",
            fill: "none",
          }}
        /> */}
      </SelectBoxWrapper>
    </React.Fragment>
  );
};

const SelectBoxWrapper = styled.div`
  display: flex;
`;

const LocationSelect = styled.select`
  width: 75px;
  height: 30px;
  margin-left: 10px;
  margin-top: -6px;
  border-radius: 6px;
  cursor: pointer;
  /* -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none; */
`;

export default LocationSelectBox;

만드는 과정은 어렵지 않았고 select 목록은 option 배열에 딕셔너리 형태로 지정해두고 map() 함수를 사용해 구현하였다. 하다보니 뭔가 별거 아닌 select box를 꾸며보고 싶다는 생각이 들었고, 라이브러리에서는 제공하지만 순수 React에서는 제공하지 않는 select box의 화살표 아이콘을 다른 아이콘으로 바꾸고 싶다는 생각이 들었다.

갑자기 욕심이 나서 이 부분에서 많은 시간을 빼았기긴 했다... 

결국 선택한건 css에서 주석 처리 된 부분처럼 화살표를 안보이게 한 뒤 내가 넣고싶던 지도 모양의 아이콘을 추가해 margin에 - 값을 줘서 위치를 조정했다.

화살표 모양
지도 모양 아이콘

위와 같이 바꿔 보았었는데 결국 직관적이지도 않고 맘에 들지 않아 원래 화살표 모양으로 다시 바꿔 놓았다... 좋은 시도였다....ㅠㅠ

 

아! 그리고 hidden 속성을 통해 '구' 와 '동'은 select box를 drop down 했을때 보이지 않도록 처리했다.

 

3. 

PostCard.js

import React from "react";
import styled from "styled-components";
import { Grid } from "../elements";

import { PostData } from "../shared/PostTest";

const PostCard = () => {
  return (
    <React.Fragment>
      <PostArea>
        {PostData.map((p, idx) => {
          return (
            <Grid is_flex margin={"12px 9px"}>
              <Post>
                <ChipDiv>
                  <CurrentChip>{p.currentState}</CurrentChip>
                </ChipDiv>
                <PostImg src={p.images} alt="PostImg" />
                <PostTitle>{p.title}</PostTitle>
                <PostContent>{p.content}</PostContent>
              </Post>
            </Grid>
          );
        })}
      </PostArea>
    </React.Fragment>
  );
};

const PostArea = styled.div`
  width: 380px;
  height: 100%;
  box-sizing: border-box;
  display: flex;
  justify-content: flex-start;
  flex-wrap: wrap;
  margin-left: -8px; // 이거 말고는 가운데로 정렬 할 방법이 없나?
`;

const Post = styled.div`
  width: 172px;
  height: 280px;
  margin: 5px auto;
  border-radius: 16px;
  border: 1px solid #888888;
  background-color: #f9f1f1;
  /* box-shadow: "5px 5px 5px rgba(255, 138, 61, 0.25)"; */
`;

const ChipDiv = styled.div`
  display: flex;
  justify-content: flex-end;
  margin-top: 3px;
  margin-right: 3px;
`;

const CurrentChip = styled.div`
  width: 56px;
  height: 22px;
  border-radius: 6px;
  text-align: center;
  font-size: 16px;
  background-color: var(--point-color);
`;

const PostImg = styled.img`
  width: 170px;
  height: 130px;
  object-fit: cover;
`;

const PostTitle = styled.p`
  font-size: 20px;
  font-weight: bold;
  margin: 4px 4px;
`;
const NickAndDate = styled.p`
  font-size: 16px;
  display: flex;
  justify-content: flex-end;
`;

const PostContent = styled.p`
  width: 168px;
  height: 70px;
  margin: 0px 4px;
  overflow: hidden;
  /* text-overflow: ellipsis; 
  white-space: nowrap; */ // overflow ... 으로 표시할까 말까
`;

export default PostCard;

post부분은 크게 문제 없이 되었다. 

어짜피 axios를 통한 api 호출을 하여 데이터를 뿌려주는 작업만 남아서 임의로 데이터를 만들어 두고 import해와서 그 데이터로 map()함수를 돌려 구현하였다.

 

 

글 작성 페이지

여기서 다른 부분은 다 해보았던 것이었고 해시태그 추가하기 부분을 짚어보려 한다.

<HashTagArea className="HashWrap">
          <CateText>#해쉬태그</CateText>
          <HashInputOuter className="HashInputOuter">
            {/* 동적으로 생성되는 태그를 담을 div */}
            <HashInput
              className="HashInput"
              type="text"
              defaultValue={tagName}
              // onChange={onChangeHashtag}
              onKeyUp={createTag}
              placeholder="태그 추가하기!"
            />
          </HashInputOuter>
        </HashTagArea>

일단 구조적으로는 맨 바깥 div가 안의 div와 input을 감싸고 있고, 안쪽의 div는 새로 생성하는 태그를 담을 div이다.

 

  const createTag = useCallback(
    (e) => {
      // if (process.browser) { process is not defined 에러 발생!! 그래서 주석처리함
      const GetHashContent = document.querySelector(".HashInputOuter"); //HashInputOuter클라스에서 입력하는 요소를 불러온다!
      const HashWrapInner = document.createElement("div"); // div 만들기
      HashWrapInner.className = "HashWrapInner";

      // 태그 클릭 시 태그 삭제
      HashWrapInner.addEventListener("click", () => {
        GetHashContent?.removeChild(HashWrapInner);
        console.log(HashWrapInner.innerHTML);
        setHashArr(hashArr.filter((tagName) => tagName)); // filter()는 주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환
      });

      // input에서 enter로 태그 생성 enter의 키 코드는 13 이다!
      if (e.keyCode === 13 && e.target.value.trim() !== "") {
        // trim()은 문자열 좌우에서 공백을 제거하는 함수
        console.log("엔터로 된거닝?", e.target.value);
        HashWrapInner.innerHTML = "#" + e.target.value;
        GetHashContent?.appendChild(HashWrapInner); // 옵셔널체이닝 Tip. 존재하지 않아도 괜찮은 대상(?.의 앞부분)에만 사용해야한다!
        setHashArr((hashArr) => [...hashArr, tagName]);
        setHashtag(""); // 태그를 추가한 뒤 새로운 태그를 추가하기 위해 tagName을 다시 빈 값으로 만들어준다.
      }
    },
    // },
    [tagName, hashArr]
  );

 

일단 위의 사진과 같이 데이터를 보내 배열로 담을 state가 1개 필요하고 또, onChange를 통해 배열 state에 저장할 문자열 state가 1개 필요하다.

 

Node 상에서 window 객체를 바로 쓰면 에러가 발생할 때가 있기때문에 process.browser로 브라우저 상태인지 확인해주면 좋다 ( 안쓰면 오류가 나는 경우도 있음 - 추후 공부 예정 )

그래서 if문의 조건으로 process.browser를 써줬는데 나는 오히려 에러가 났다...그래서 일단 주석처리를 하고 조건을 뺀 뒤 진행을 해보았다.

 

일단

document.querySelector(' ')는 괄호안의 className을 갖고있는 인풋에서 입력하는 요소를 불러오는 역할을 한다.

그리고 document.createElement("div")를 통해 div를 만들어 주고 나중에 생성되는 div의 css를 다루기 위해 className도 붙여주도록 한다.

 

태그를 삭제 할 방법으로는 

태그를 클릭했을 시 removeChild()를 통해 해시태그 div를 삭제해 주기로 하고  filter()로 통해 기존 hashArr useState 배열에서 해당되는 hashtag를 제거한 배열값만 반환해주도록 했다.

 

그리고 태그 생성은 input 에서 enter를 입력했을때 생성 되도록 했고, trim()함수를 통해 input에서 입력하는 문자열의 양쪽에 공백이 있더라도 문자 부분만 가져오도록 했다. 

그리고 태그 앞에 '#'을 붙여주며 appendChild()를 통해 해시태그 div를 생성한다.

그리고 hashArr배열에 새로운 해시태그 내용을 추가해주고 setHashtag('')를 통해 태그를 추가한 뒤 새로운 태그를 다시 추가해줄 수 있도록 input에 입력했던 값들이 비어지도록 설정해놨는데, 

뭐가 문제인건지 지금은 맨 마지막 부분이 잘 작동하지 않아 원인을 찾아보고 있는 중이다.

 

아! 그리고 중간에 옵셔널 체이닝(?.)을 활용하였는데, 

옵셔널 체이닝은 존재하지 않아도 괜찮은 대상(?.의 앞 부분)에만 사용해야한다.

만약 존재하지 않는다면 undifined나 null로 반환이 될 것이다.

 

 

Comments