TIL_52_1차 프로젝트 후기

JIEUN·2021년 3월 27일
9
post-thumbnail

우리가 만든 페이지부터 자랑해보자!

로그인/회원가입 페이지

  • 아이디 이메일 형식에 따른 아이디, 비밀번호 8자 이상일 때 버튼 활성화
  • 필수 입력을 하지 않았을 경우 회원가입이 되지 않게 함.
  • 존재하는 이메일일 경우 회원가입 불가능,
  • 로그인 시, 가입되지 않은 아이디 일 경우 메인으로 이동되지 않게 함.

메인 페이지

  • 네브 바 및 푸터는 공용 컴퍼넌트로 재사용 가능하도록 구현
  • 메인페이지의 이미지 슬라이드 기능은 리액트 슬릭을 통하여 구현
  • 메인페이지의 각 피드와 아이템 페이지의 이동은 URL PATH Params를 통하여 동적라우팅 구현
  • 메인페이지의 로그인 및 공유 모달창 구현
  • infinite-scroll-component로 무한스크롤 기능 구현, 데이터 4개씩 fetch
  • 상품 유무에 따라 조건부 렌더링으로 피드 구현

피드 상세 페이지

  • 기존에 입력되어 있는 댓글 데이터 fetch
  • 댓글 입력 시 버튼 활성화 기능 구현
  • 댓글창에 댓글 입력 시 댓글 추가 기능 구현
  • 댓글 좋아요 및 카운트 기능 구현
  • 댓글 삭제 기능 구현
  • 입력한 댓글 DB에 POST 후 리스트 업데이트 기능 구현

캐릭터/카테고리 페이지

  • 캐릭터별 클릭 시, 쿼리 파라미터를 적용하여 동적라우팅 기능 구현
  • 최신순, 가격 높은 순, 가격 낮은 순 정렬은 쿼리 파라미터를 적용하여 동적라우팅 기능 구현
    (정렬에 변화가 생길 때 마다 변화가 되는 쿼리 파라미터 api를 요청하여 데이터를 랜더링.)
  • 상품 클릭 시, 동적라우팅 기능을 이용하여 해당하는 상품 상세 페이지로 이동할 수 있게 함.

검색 모달창

  • 캐릭터 아이콘에 쿼리 파라미터를 적용한 동적라우팅 기능 구현
  • 검색 인풋창에 입력 시, 해당하는 키워드 필터링 기능 구현

제품상세 페이지

  • 공유 버튼 클릭 시 모달창 구현
  • 슬릭 라이브러리를 이용하여 이미지 슬라이드 기능 구현
  • 맵 함수를 통하여 받아온 데이터를 이미지화 시킴.
  • url path params를 통해 해당 제품 페이지 동적 라우팅 기능 구현
  • 리뷰 리스트에서 좋아요 버튼 활성화 기능 구현

카카오 프렌즈샵 클론을 하게 되다. 그리고 PM을 맡게 되다.

프론트엔드에는 준우님, 성훈님, 나은님 그리고 나 이렇게 네 명과 백엔드에는 왕록님, 정원님 까지 총 6명이 한 팀이 되었다.

일단 나는 모든게 두렵고 자신이 없었다. 어떻게 백엔드와 소통이 되는지 그 구조 자체도 온전히 이해가 이루어지지 않은 상태였고 과제를 하나씩 진행해나갈 때마다 나는 다른 이들보다 느리다는 것에 많은 압박감을 느끼고 있던 때였다.

과연 내가 이들 안에서 수치적으로 '얼만큼' 내 몫을 해낼 수 있을까 하는 걱정이 앞섰다. 내가 선택했던 사이트가 지정이 되어서 얼떨결에 PM을 맡게 되다보니 (사실 큰 의미가 없는)직책으로 인한 책임감이 부담스럽게 느껴졌다.


잘 모르는 상태에서 하려다보니, 최대한 알려준 대로 따라가 보기로 했다. 그래서 트렐로를 적극적으로 활용했다. 뭐든 다 적고 매일 아침마다 스탠드업 미팅을 하며 트렐로에 기록을 했다. 아무래도 내가 프론트엔드이다 보니, 백엔드 쪽으로 신경을 못써드려서 개인적으로 죄송한 마음이다. 그저 진행상황을 얘기해주는 것 말고는 할 수 있는게 없었다.

첫 주에는 각자 구현할 페이지를 하나씩 맡아서 진행하기로 했다.

  1. 로그인/회원가입 페이지 2. 메인 페이지 3. 피드 상세 페이지 4. 검색/ 카테고리 페이지
  • 여기서 아쉬운 점
  1. 서로 연결되는 페이지를 고려하지 않고 그냥 페이지로 나눠서 각자 진행하다보니, 나중에 합치면서 일어나는 문제가 너무 많아졌다.
  2. 커머스 샵임에도 주요 기능이 많은 사이트라, 위의 페이지들에 집중하다보니 장바구니를 후순위로 두어서 결국은 구현해내지 못한 것이 너무 아쉽다.

나름대로 언제까지 레이아웃을 완성하고 -> 그 후에는 기능 구현을 한 뒤, -> 백엔드와 맞춰보자! 란 식으로 큰 틀을 잡고 진행을 하였는데

  • 여기서 아쉬운 점은
  1. 내가 막상 기능 구현을 했어도, 그게 백엔드와 맞춰보면서 필요가 없어지는 기능인 경우도 있고 내가 기능 구현만 해서 되는게 아니라 백엔드와 맞춰서 진행을 했어야 하는 기능들이 있어서 결국 한 것을 전부 수정해야하는 상황이 발생하게 됐다.

(예를 들어, 댓글이 최신순으로 달려야 해서, 나는 단순하게 댓글 리스트 맵 함수에 reverse를 사용하여 댓글이 역순으로 달리도록 기능을 구현했는데, 백엔드에서 최신순으로 데이터를 보내줄 수 있어서 굳이 내가 역순으로 정렬할 필요가 없었고, 댓글을 달고 삭제하기 위해서는 로그인을 해서 인증/인가를 거친 상태여야 가능하다보니 내가 삭제 기능을 구현한다고 해결되는 것이 아니라, 백엔드에게 fetch 메소드 delete를 요청해야 가능한 것이었다.)

  • 그래서 중요한 것은
    백엔드/ 프론트엔드를 나누어 각자 맡은 것을 각자 진행하는 것이 아니라, 내게 뿌려줄 데이터를 맡은 사람과 의견을 나누는 시간이 필요하다는 것을 깨달았다. 백엔드에게 보내야 하는 데이터가 있고 받아야 하는 데이터가 있는데 그걸 별개의 문제로 분리하여 진행을 하는 것은 정말 잘못된 거였다.

사실 이런 부분들을 PM이 주도적으로 이끌었어야하는데 백엔드에 대한 지식도 부족하고 프로젝트 자체가 처음이다보니 정말 너무너무나 서툴었고 그래서 우리 팀원들에네 너무너무 미안한 마음이다.. 😭

분명 쉬지않고 코딩만 한 것 같은데 결과물은 왜 어설픈걸까?


공감 됐던 짤이라 첨부해본다.

내가 댓글 상세 페이지를 구현하면서 모르는 것이 너무 많아, 깃허브에서 우리 동기들의 위스타그램 코드를 다 뜯어봤다. 생각보다도 대부분의 동기들이 필수 기능뿐만 아니라 추가 기능들을 다 구현해놨었고 배운 세션을 참고하여 코드의 완성도를 높인 동기들이 너무나 많았다.
다시금 나의 안일함과 부족함을 절실히 깨닫는 시간이었다.
더 열심히, 그리고 더 투자하여 공부를 해야겠다는 다짐을 가지게 되었다.

사실 내가 맡은 댓글 상세페이지는 이전에 진행했던 위스타그램 과제와 비슷한 부분이 굉장히 많았다. 그럼에도 위스타그램에서 간신히 필수기능만 구현했던 나에게는 어려운 과제였다.

기억해두고 싶은 코드를 잠깐 기록해보겠다.

//SubNav.js
class SubNav extends React.Component {
  constructor() {
    super();
    this.state = {
      toggleOn: false,
    };
  }

  toggleOn = () => {
    this.setState({
      toggleOn: true,
    });
  };
  toggleOff = () => {
    this.setState({
      toggleOn: false,
    });
  };
/*네브 바에서 돋보기 아이콘을 누를 시, 
search bar 모달창을 띄우기 위해 작성한 함수.
나은님의 도움을 받아 좀 더 명확하게 이해할 수 있게 되었다.*/
  render() {
    return (
      <div className="wrapMain">
        {this.state.toggleOn ? (
          <Search toggleOff={this.toggleOff} />
          //search component 안에 props를 지정해주었음.
        ) : (
          <div className="mainNav">
            <div className="navBarFirst">
              <Link to="/">
                <FaChevronLeft size="22" />
                <FaHome className="homeIcon" size="22" />
              </Link>
              <h1 className="title"> {this.props.title} </h1>
      /*이 부분은 각 페이지마다 sub nav의 구조는 같으나, 
 제목이 달라서 제목 부분만 props로 보내주고 있는 것을 알 수 있다.*/
              <div>
                <button className="navIcon" onClick={this.toggleOn}>
                  <FaSearch size="22" />
                </button>
                <button className="navIcon">
                  <FiGlobe size="22" />
                </button>
              </div>
            </div>
          </div>
        )}
      </div>
    );
  }
}
//MainNav.js
  <ul className="navBarSecond">
              {menuList.map(menu => (
                <Link to="/finish">
                  <li className="width">{menu}</li>
                </Link>
    /*이 부분은 메인 네브 바의 카테고리가 반복됨으로 
    맵 함수를 이용하여 간략하게 줄인 것이다.*/
    
              ))}
            </ul>
          </div>
        )}
      </div>
    );
  }
}

export default MainPageNav;

const menuList = ['오늘', '신규', '인기', '마이'];
//MainNav.scss
      .width {
        display: flex;
        justify-content: center;
        width: 160px;
        font-size: 16px;
        line-height: 40px;
        &:hover {
          border-bottom: black solid 4px;
        }
      }
//메인 네브바에 준 호버 효과
//Routes.js
<Route exact path="/feed/:id" component={Feed} />
//Feed.js
componentDidMount() {
    fetch(`http://192.168.200.131:8000/feed/${this.props.match.params.id}`)
  /*match.params를 이용하여 페이지마다 
  다른 데이터를 각각 받아올 수 있도록 지정.*/
      .then(res => res.json())
      .then(res => this.setState({ content: res.result }));
  }

  pressEnter = async e => {
    await fetch(
      `http://192.168.200.131:8000/feed/reply?feed_id=${this.props.match.params.id}`,
      /*페이지 마다 각각 다른 댓글 데이터를 보내야하므로 
      여기 또한 match.params 를 이용했다.*/
      {
        method: 'POST',
        body: JSON.stringify({
          id: this.state.id,
          content: this.state.value,
        }),
      }
    )
      .then(res => res.json())
      .then(res => res.status);

    e.preventDefault();
    if (this.state.value === '') {
      alert('내용을 입력해주세요');
      return;
    }

    this.setState({
      commentList: this.state.commentList.concat([
        {
          userId: this.state.id,
          content: this.state.value,
        },
      ]),
    });
  };
//Feed.js
  render() {
    const settings = {
      dots: true,
      infinite: false,
      speed: 500,
      slidesToShow: 1,
      slidesToScroll: 1,
    };
    const title = '게시물';
    const {
      content,
      isLoginModalView,
      heartColor,
      isShareModalView,
    } = this.state;
    const changeHandleBtnColor = this.state.value.length >= 1;
    return (
      <>
        <SubNav title={title} />
        //props 지정.
        <div className="feedPage">
          <div className="feedBox">
            <div className="feedBoxHeader">
              <div className="feedBoxHeaderImg">
                <img
                  className="headerImg"
                  src={content?.profile_picture}
      /*백엔드로부터 받은 데이터의 객체 내의 속성값에
          접근하기 위해 옵셔널 체이닝을 이용하였다.*/
                  alt="이미지"
                />
              </div>
              <div className="nameAndTime">
                <div className="characterName">{content?.username}</div>
                <div className="time">{content?.datetime}</div>
              </div>
            </div>
            <StyledSlider className="feedBoxImg" {...settings}>
              {content.image_url?.map((list, index) => (
                <img key={index} className="mainImg" src={list} alt="이미지" />
              ))}
            </StyledSlider>
      //이미지 슬라이드 기능은 라이브러리를 이용
            <div className="feedBoxIcon">
              {isLoginModalView ? '' : ''}
              {heartColor ? (
                <div className="heartIcon">
                  <button onClick={this.colorChangeBtn}>
                    <FaRegHeart size="24" />
                  </button>
                </div>
              ) : (
                <div className="heartIconColorChange">
                  <button onClick={this.colorChangeBtn}>
                    <FaHeart color="red" size="24" />
                  </button>
                </div>
              )}
              <div className="chatIcon">
                <button onClick={this.goToFeedDetail}>
                  <BsChat size="24" />
                </button>
              </div>
              <div className="replyIcon">
                {isShareModalView && (
                  <ShareModal shareHandleModal={this.shareHandleModal} />
                )}
                <button onClick={this.shareHandleModal}>
                  <BsReply size="32" />
                </button>
              </div>
            </div>
            <div className="feedLikeCount">
              좋아요
              <span className="feedLikeCountUpDown">{content?.like_count}</span></div>
            <p className="feedContentTitle">{content?.title}</p>
            <p className="feedContent">{content?.content}</p>
          </div>

          <Comment
            value={this.state.value}
            inputComment={this.inputComment}
            changeHandleBtnColor={changeHandleBtnColor}
            pressEnter={this.pressEnter}
          />

          {content.reply?.map(comment => (
//댓글의 정보들을 맵 함수를 이용해 돌리고, 
          //props를 통해 자식 component에게 보내줌
            <CommentBox
              key={comment.id}
              name={comment.reply_username}
              text={comment.reply_content}
              likeCount={comment.like_count}
              createdAt={comment.datetime}
              handleCommentDelete={this.handleCommentDelete}
            />
          ))}
        </div>
        <Footer />
      </>
    );
  }
}

export default Feed;

const StyledSlider = styled(Slider)`
  ul.slick-dots {
    margin-bottom: -20px;
  }

  .slick-prev {
    poacity: 0.6;
    margin-left: 45px;
    z-index: 9;
  }

  .slick-next {
    margin-right: 60px;
    poactiy: 0.6;
  }

  .slick-prev:before {
    color: black;
    font-size: 30px;
  }
  .slick-next:before {
    color: black;
    font-size: 30px;
  }

  .slick-disabled {
    display: none !important;
  }
`;

MERGE가 뭐지? 머지가 왜 머지?

둘째 주에는 정말 충돌의 전쟁이었다. 기능 구현도 기능 구현인데 충돌을 해결하느라 많은 시간을 할애했다.

  • 그래서 깨달은 점은
    페이지마다 공통적으로 쓰이는 네브 바나, 푸터는 먼저 다 만들어놓고 merge까지 미리 해놓는 것이 좋다.
    처음 git pull origin master 를 해보고, merge를 해보고 여러번의 충돌을 겪고나니 깃허브가 더 이상 어렵게 느껴지지 않게 되었다..!

그리고, merge로 인한 충돌 뿐만 아니라.. 각자 만들고 있던 페이지가 하나의 단독적인 페이지가 아니고 연결되는 페이지였어서 merge를 하고보니 수정할 내용들이 계속 생기고 최상단이었던 component 위로 또 부모가 생기는 경우가 발생해서 부모였던 component는 자식이 되고 구조가 엉켜버려서 그거 해결하는데도 꽤 애먹은 기억이 난다.

독단적으로 진행하는 기능 구현은 의미가 없다.. 무조건 소통해야 한다.


여러 브랜치를 생성하여 이동하고 수정하고 푸시하여야 하다보니, 잦은 커밋이 발생하게 되었다ㅠ 거기다 충돌 해결하고 머지하면 또 충돌이 나서 해결하고를 반복.. 허허허 '최종 진짜 진짜 최종'의 연속이었다.

그래서 이번 프로젝트를 통해 얻은게 뭔데?


왕록님과 정원님의 완성된 모델링!
진짜 우리팀의 백엔드는 최강자였다. 데이터는 열심히 주시는데 왜 받아먹질 못하니.. ㅠㅠ 제대로 활용하지 못해서 죄송한 마음 뿐.. 부탁할 때마다 흔쾌히 들어주시고 항상 파이팅 넘치게 긍정적으로 도와주셔서 정말 감사했다. 정말 좋은 분들!!! 항상 오께이~~ 오케이~~ 하시면서 다 가능하고 다 된다고 서포트 해주셔서 너무 고마웠어요!!!!!

직접 캐릭터를 그려서 제공해준 나은님.. 최고.. 너무 귀여워오.. 계속된 실패에 좌절해있을 때마다 서로 할 수 있다고 응원해주던 우리 프론트.. 진짜 2주 내내 즐겁게 프로젝트에 임할 수 있었다. 그리고 자기 일 처럼 옆에서 도와주셔서 좋은 자극도 넘 많이 받았다. 우린 언제나 할 수 있어빌리티~~

기능 구현을 하면서 나의 부족함을 절실히 깨달았고, 동시에 쓰면서도 헷갈리던 것들을 명확하게 배울 수 있는 시간이었다. 내가 데이터를 받고 동시에 요청하면서 화면에 랜더될 때 그 짜릿함! 함수 한 줄에 달라지는 화면을 보고 얻는 성취감! 그리고, 위에는 프론트엔드 목표인데 이 전에 나는 해내지 못했던 것들이었는데 이번 프로젝트 통해서 목표를 이루었으니 스스로를 칭찬해본다.. 쓰담쓰담.

12개의 댓글

comment-user-thumbnail
2021년 3월 28일

지은님 마지막 사진 ㅋㅋㅋㅋ(시선강탈!)
수고 많으셨어요 지은님 😋👏🏻

1개의 답글
comment-user-thumbnail
2021년 3월 28일

수고 많으셨어요 지은님!!

1개의 답글
comment-user-thumbnail
2021년 3월 28일

우리PM 짱이얏!!!!!!!!!!!!!!! 지은님 고생 많았어빌리티요!! 2차 프로젝트도 할 수 있어빌리티!!
후기 쓰고 나니까 지은님 한 거 엄청 많던데 티가 안나서 속상할 것 같아여ㅜㅜ
지은님 짝꿍이라서 지은님이 우리팀 PM이라서 넘넘 좋았어요 2차도 빠이팅

1개의 답글
comment-user-thumbnail
2021년 3월 29일

완전 멋진 PM님 고생 많으셨습니다!!!
2차도 같이 화이팅해봐요!!!ㅎㅎㅎㅎㅎㅎㅎ

1개의 답글
comment-user-thumbnail
2021년 3월 30일

지은님 고생하셨어요 !! 2차 프로젝트도 파이팅~!!

1개의 답글
comment-user-thumbnail
2021년 4월 2일

앜!! 캐릭터 아이콘 직접 그리신거 너무 귀엽네요 ㅠ_ㅠ 잘 보고 갑니다~!

1개의 답글