2차 프로젝트 후기

choice·2021년 7월 31일
1

프로젝트

목록 보기
2/2

wecode 2차 팀프로젝트로 팀원 5명으로 구성되어 구직/알선 사이트인 Wanted를 모티브로 한 사이트를 제작 하였다.

  • 기간: 2021.7. 19.~30. (2주)
  • 백엔드: 박성준, 이원석
  • 프론트엔드: 박관용, 최민재, 최창원

💡 프로젝트 방식 "SCRUM"

  • Sprint: 1주일
  • Daily Stand UP Meeting: am 11:00
  • Use Tool: Notion

🏇1차 스프린트 회의

1.Team 목표

  • Pages: 메인(기존 탐색페이지 활용), 공고 상세 페이지, 이력서 작성 페이지(지원하기)

  • Data: 모델링, 이력서CRUD, 파일 업로드
    -추가구현: 연봉별 차트

    회의를 통해 나는 메인 페이지를 담당하게 되었고 세부적으로는 슬라이드, 필터링, 무한 스크롤 기능을 구현하게 되었다.

#Main Page

기획단계에서 기존의 Main Page에 있는 커머스적인 요소를 구현하지 않기로 가닥을 잡았고 간단한 슬라이더와 각종 필터기능을 겸비한 공고 리스트를 메인화면으로 구현하자고 얘기가 되었다.

내가 담당하는 부분을 기능단위로 나누어 보면 크게 3가지로
1. 메인 슬라이더
2. 필터
3. 무한 스크롤
이렇게 나누어 볼 수 있었고, 각 기능단위별로 코드와 함께 정리해 보고자한다.

1. Slider


1차프로젝트 때 직접 슬라이더를 코드로 구현해 보았기 때문에 이번엔 라이브러리를 활용해서 구현해 보고 싶었다, 결론부터 말하자면 실패!라고 평가할 수 있다.

🧑🏻‍💻 Code Point

import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore, { Navigation, A11y, Autoplay } from 'swiper';

SwiperCore.use([Navigation, Autoplay, A11y]);

function MainSlider() {
  return (
    <>
      <Swiper navigation {...settings}>
        {SLIDE_IMAGE.map((image, index) => (
          <SwiperSlide key={index}>
            <Slide url={image.url} />
          </SwiperSlide>
        ))}
      </Swiper>
    </>
  );
}

export default MainSlider;

const settings = {
  slidesPerView: 'auto',
  loop: true,
  loopAdditionalSlides: 1,
  autoplay: {
    delay: 5000,
  },
};

코드는 라이브러리를 활용했기 때문에 매우 단순하다, 특징적인 것은 없고 라이브러리 활용 안내대로 core가져와 심어주고(use) 필요한 세팅을 넣고 실행하면 끝! 매우 단순하고 간단한데 내가 실패라고 얘기한 까닳은 컨트롤을 하기 어렵기 때문이다.
편하게 고급 기능을 구현하고 싶기에 사용을 했는데 오히려 라이브러리에 맞춰 코드나 페이지 구성이 맞춰줘야하는 느낌이랄까? 조금이라도 변경하고 싶거나 세밀한 부분을 커스터마이징 하기에는 내가 가진 내공이 부족하여 손대기 어려웠고, 분명 제대로 작동하는 것처럼 보이지만 간혹 랜더링이 이상해지면 배치가 이상해진다거나 하는 문제들이 생겼고, 수정하고싶어도 어디서 부터 손을 봐야하는지 모르는 답답함에 지쳐버렸었다.
라이브러리를 제대로 사용하려면 라이브러리를 뜯어서 살펴보고 입맛에 맞게 수정할 수 있을 정도의 실력이 필요한거 같고 아이러니하게 이런 상황이면 직접 만드는게 더 좋을 수 있는 상황이 아닐까? 제일 아쉬운 부분은 라이브러리를 제대로 사용하기 어려워 직접 만들어보려 했지만 다른 상황들에 밀려 시도조차 하지 못하고 부족한 활용 그대로 프로젝트를 마무리 해야됬다는 것, 이번 프로젝트에서 리팩토링을 여러군대 해야하겠지만 아마 1순위로 손대는 것이 메인 슬라이더가 아닐 듯 싶다.

2. Filter


필터.. 참 탈도많고 우여곡절도 많고.. 다시 돌아온 블랙홀이랄까?(조금 더 자세한 감정은 블랙홀에 빠지다..글 참조) 이번 프로젝트에서 가장 공을 들이고 또 가장 힘들고, 가장 많이 남은(배운)것이 이 filter기능일 것 같다.
(이 후기를 쓰는 와중에 버그가 발견되어, 수정을 했다... 정말 블랙홀이다.. 끝나지 않을 것 같은..)

🧑🏻‍💻 Code Point (지저분한 코드는 블랙홀에 빠지다 참조)

//state값은 3개로 우선 백엔드에서 받은 데이터를 기능 구현을 위해 구조를 바꿔서 받은 filterCategories(가장 핵심 포인트), 선택된 category에 따른 tagList를 보여주기 위한 selectCategory, 마지막으로 중복선택이 안된다는 알림을 표현하기 위한 multipleAlert이다.
  const [filterCategories, setFilterCategories] = useState([]);
  const [selectCategory, setselectCategory] = useState(1);
  const [multipleAlert, setMultipleAlert] = useState(false);

  useEffect(() => {
			...
        setFilterCategories(
          data.result.map(({ ...rest }) => ({
            ...rest,
            //가장 중요한 각 category별로 선택된 tag를 담기위한 selected배열 생성
            selected: [],
          }))
			...

  
              
      //선택한 tag가 속한 categry의 selected를 return하기 위한 함수        
  const prevSelected = id => {
    return filterCategories.filter(filterList => filterList.id === id)[0]
      .selected;
  };
            
      // 중복 선택이 안되는 tag는 다른 tag가 선택되면 바꿔(change)줘야 한다
  const changeTag = (id, tag) => {
    return filterCategories.map(mapList =>
      mapList.id === id ? { ...mapList, selected: [tag] } : mapList
    );
  };

	//기본적으로 선택된 tag를 추가(add)하기 위한 함수(중복선택 가능 태그와 아닌것에 구분이 시작된다.)
  const addTagList = (id, tagName, multiple) => {
    if (multiple) {
      setFilterCategories(
        filterCategories.map(mapList =>
          mapList.id === id
            ? { ...mapList, selected: [...prevSelected(id), tagName] }
            : mapList
        )
      );
    } else if (
      !multiple &&
      prevSelected(id).length === 0 &&
      tagListCount() === 3
    )
      alert('태그 선택은 3개만 가능 합니다.');
    else if (!multiple && prevSelected(id).length === 0)
      setFilterCategories(changeTag(id, tagName));
    else {
      setMultipleAlert(true);
      setFilterCategories(changeTag(id, tagName));
    }
  };

	//모든것에 우선시 되는 조건으로 선택된 태그를 다시 누를경우 지워(remove)줘야 한다.
  const removeSelected = (id, tagName) => {
    const removeTag = prevSelected(id).filter(prevTag => tagName !== prevTag);
    setFilterCategories(
      filterCategories.map(mapList =>
        mapList.id === id
          ? {
              ...mapList,
              selected: [...removeTag],
            }
          : mapList
      )
    );
  };

	//총 선택된 tag의 수를 알아야 최상단에 표시도 되고 3개 이상 선택 할 수 없는 알림을 띄울 수 있었다.
  const tagListCount = () => {
    return filterCategories
      .map(setList => setList.selected.length)
      .reduce((a, b) => a + b, null);
  };

	//갖고있는 함수들을 활용해서 tag가 선택(pick)됬을 경우 상황에 맞는 함수를 실행시켜 준다.
  const pickTagHandler = (id, tagName, multiple) => {
    if (prevSelected(id).includes(tagName)) removeSelected(id, tagName);
    else if (tagListCount() === 3 && multiple)
      alert('태그 선택은 3개만 가능 합니다.');
    else addTagList(id, tagName, multiple);
  };

  
		//선택된 tag를 한곳에 모아 query params로 활용하였다.
  const makeTagQuery = () => {
    const tags = filterCategories.map(setList => setList.selected);
    const tagQuery = tags.flat();
    return tagQuery;
  };

단순히 비교하면 전의 코드에서 약 1/3로 코드를 줄일 수 있었고, 중복되는 코드를 함수로 구분하여 효율적이게 사용할 수 있었다. 하고 난 후에 보면 어렵지 않은 코드들이지만 완성을 위해 거의 5일이라는 시간을 소비한것 같다.(다시하고..다시하고..다시하고...)
이를 통해 내가 뼈저리게 느낀점은

"기능 구현을 시작하기 전에 충분한 고민과 흐름을 정리해야한다"

이다,

자신감이 붙어서일까? 재미가 있었어서 일까, 기능을 구현하기위해 떠오르는 코드들을 무작정 쳐내려가기 시작하고 그 과정에서 점점 늘어가는 로직이 쌓이다보면 그 전에 안좋은 코드 처럼 기능은 되지만 어느 한곳이라도 무너지면 돌이킬 수 없는? 새로하는것이 빠를 수 밖에 없는 상황이 발생하고 말았다. 능숙한 개발자라면 고민하는 시간을 단축 시킬 수 있고 또 변화에 따른 대응이 유연하겠지만 나는 아직 초보 개발자로 충분한 고민을 했다 하더라도 수정하는 일이 무조건 발생 할 텐데 조금만 탬포를 늦추고 완벽히 준비하고 시작하는 것이 시간을 더 아낄 수 있지 않을까? 급할수록 돌아가라 어느 현인의 말이 너무도 와닿는 시간이었다.

3. 무한 슬라이드


이번 프로젝트는 돌이켜보면 정말 우여곡절이 없는 기능이 하나도 없는 듯 하다, 무한 슬라이드 또한 한번 포기하고 페이지네이션으로 갔다 다시 돌아온 기능으로 그래도 다행이 최소한 만족할 수 있는 기능을 구현한 것 같다.

🧑🏻‍💻 Code Point

//로딩을 알리는 div를 보여주고 감추기 위한 state값
const [isLoading, setIsLoading] = useState(true);

//백엔드 데이터를 받을 때 total count를 저장하기 위한 ref값
  const listCount = useRef(0);
//fetch 주소 offset에 들어가는 값, scroll에 따라 증가한다.
  const offsetNumberRef = useRef(0);
//IntersectionObserver가 감지(observe)하기 위한 타겟
  const fetchTarget = useRef(null);

  //observe가 true(화면에 보일 때) 실행되는 함수, offset에 들어가는 숫자를 증가시키고 fetch함수를 실행 시킨다.(가장 중요, state값을 변경하는 것이 아니기 때문에 렌더링을 강제로 일으 킬 수 없기에 fetch 함수를 이곳에서 실행시켜 데이터를 받아온다. 자세한건 블로그내 뼈가되고 뼈가되는 글 참조) 
const onIntersect = ([entry]) => {
    offsetNumberRef.current = offsetNumberRef.current + 20;
    console.log(
      `fetch`,
      `${makeQuery(tagFetchData)}&offset=${offsetNumberRef.current}&limit=20`
    );
    if (entry.isIntersecting) {
      fetch(
        `${API.JOBPOSTING}?${makeQuery(
          tagFetchData
        )}&offset=${offsetNumberRef.current}&limit=20`
      )
        .then(res => res.json())
        .then(data => {
          setpostListData(prev => [...prev, ...data.result]);
          listCount.current = data.count;
          console.log(`data.result`, data.result);
        });
    }
  };

//최초의 IntersectionObserver API를 생성 해 주는 함수
  useEffect(() => {
    const observer = new IntersectionObserver(onIntersect, { threshold: 0.5 });
    observer.observe(fetchTarget.current);
    return () => observer.disconnect();
  }, []);

//fetch 데이터 보다 요청하는 offset이 커질 경우 loading을 감춰 준다
  useEffect(() => {
    if (listCount.current < offsetNumberRef.current) setIsLoading(false);
  }, [postListData]);

     ...
      <PostList data={postListData} />
  //posting이 쌓이는 아래에 로딩을 생성해 놓고 타겟을 걸어 보일경우 데이터 fetch를 시킨다.
      {isLoading && <Loading ref={fetchTarget}> Loading...</Loading>}

4. 기타(styled component)

import React from 'react';

import styled from 'styled-components';

const Text = props => {
  return <Span color={props.color}>{props.children}</Span>;
};

export default Text;

const Span = styled.span`
  font-size: ${props => props.size};
  color: ${props =>
    props.oneted ? props.theme['oneted' + props.color] : props.color};
  font-weight: ${props => props.bold};
`;

컴포넌트로 재활용 가능한 스타일 생성, 택스트의 크기와 색상을 다룰 수 있는 스타일을 컴포넌트화 시켜 재사용 하였다.
추가로 공용 컬러인 onetedGray, onetedBlue 또한 적용될 수 있도록 코드를 작성했다.

const CategoryList = styled.span`
  border: 1px solid
    ${props => (props.id === props.border ? props.theme.onetedBlue : 'none')};
  color: ${props =>
    props.text.length === 0 ? 'black' : props.theme.onetedBlue};
  background-color: ${props => props.bacgourndColor};
`;

const SelectCategoryList = styled.span`
  color: ${props =>
    props.textStyle.includes(props.text) ? props.theme.onetedBlue : 'black'};
`;

선택된 데이터를 활용해(id) 스타일을 주었고, 데이터가 포함되어잇는지(includes)를 활용해서 스타일을 줄 수 있는 점이 좋았다.

스타일드 컴포넌트 도한 함수형 컴포넌트와 마찬가지로 처음 접하는 개념이었는데 sass와 달리 직관적이게 익히기는 힘들었지만 어느세 적응해서 다양한 시도들을 해볼 수 있었던 것 같다, 종합적인 소감은 처음에는 왜쓰지 싶었는데 추후에는 완전히 빠져버렸달까? 내가 선택할 수 있는 상황이라면 무조건 styled component를 선택할 것 같다.

마무리👔

  • 함수형 컴포넌트와 스타일드 컴포넌트에 확실하게 입문 할 수 있는 프로젝트였다.
  • 1차 프로젝트는 '내가 배운걸 이렇게 쓰는거구나' 라고 생각할 수 있었는데 2차프로젝트 때는 '아, 이렇게 해야 겠구나' 라는 것을 느꼇다.
  • 모든것이 조금씩 아쉬움이 남는 프로젝트였다.

Thanks to..👏

무엇보다 프로젝트 내내 하드캐리한 관용님, 싫은소리 한번 안하고 묵묵히 해준 민재님, 미안하다고 계속 신경써준(사실 저도 똑같은데..) 원석님, 어려워보이는 기능들을 뚝딱뚝딱 해낸 성준님 중간에 멘탈 나가서 방황하고 이래저래 집중 못한것 같은 저를 잘 끌고 프로젝트 마무리해준 팀원 모두 너무 감사합니다! 다음에 만나게되면 더 멋진 프로젝트 해볼 수 있었으면 좋겠습니다.
감사합니다!🙏

2차 프로젝트 회고록

profile
'앞'을 향해 가면서 꾸준히 '흔적'을 남기는 개발자

2개의 댓글

comment-user-thumbnail
2021년 7월 31일

라이브러리 사용법은 정말 넘나 어려운것...😢 고생하셨어요 창원님!! 몰래 보다 갑니당 =3

1개의 답글