1차 프로젝트 회고록

김민재·2021년 10월 19일
1

Archive Project

목록 보기
2/5
post-thumbnail

1. 💻 Team Thisisnevercode 📡

  • 안녕하세요 저희는 'Thisisneverthat' 사이트의 클론 코딩을 진행하게 된 Thisisnevercode입니다.

1_1> 👇 아래 시연 영상이 재생됩니다. (Thisisnevercode)

thisisnevercode 유튜브에서 보러가기

API DOCUMENTARY 보러가기

1_2> 🌟 프로젝트 소개

  • 프로젝트 클론 사이트 소개 : 디스이즈네버댓(THISISNEVERTHAT)은 스포츠 앤 스트릿을 컨셉트로 2010년에 론칭한 브랜드입니다.
  • 프로젝트 목표들 : Wecode foundataion에서 배운 내용들을 적용하여 사이트 구축하기
  • 진행기간 : 2021년 10월 1일 ~ 2021년 10월 16일
  • 프로젝트 참여자 : 김동권, 👨‍💻김민재👨‍💻, 김태규, 김휘민, 윤창현

2. 🛠 기술 스택

2_1> ⛏ FrontEnd 기술 스택

  • React
  • Router
  • Sass
  • Restful API
  • Git & GitHub

2_2> 🔧 BackEnd 기술

  • Node.js
  • Express
  • MySQL
  • Bcrypt, JWT
  • Git & GitHub

3. ⚙️ FrontEnd & BackEnd 구현 기능

3_1> ⛓ FrontEnd 역할

리스트 페이지 담당 [ FrontEnd ]
1. 영역 별 호버 효과
2. 색상 별 이미지 전환
3. 반응형
4. 무한 스크롤
5. gototopbutton

3_2> 🔩 BackEnd 역할

  • 회원가입 및 로그인 API 담당 [ BackEnd ]
  1. access token를 활용한 회원가입, 로그인과 로그아웃 기능
  2. 전역 에러 핸들링 생성
  3. util 폴더 생성 및 공통함수 생성

4. 📐 modeling

  • 1> products 테이블을 중점으로 리스트 페이지와 상세 페이지에 들어가게 될 다양한 이미지들을 관리하기 위해서 2> detail_images 테이블과 3> sub_imgaes 테이블을 생성하였으며 네브바에 들어가게 될 메인 4> categories 테이블과 메인 카테고리 범주에 속하는 5> subcategories 테이블 1:N 관계로 매핑하였으며 6> users 테이블에는 리스트 페이지에있는 sort 정렬 박스 중 trending과 관련된 속성 값을 나열하기 위해 넣은 7> orders 테이블이 존재한다.

5. Project Architecture

Layerd Pattern으로 설계한 이유

  • 대표적인 설계 방식인 MVC 패턴의 경우 큰 서비스의 관점에서 보았을 때 오로지 Controller와 Model 레이어로만 로직을 분리하고 있습니다.
    따라서 MVC패턴을 프로젝트에 적용하는데 있어 1> 코드의 복잡성과 2> 레이어 하나가 담당하는 비중이 너무 커진다는 단점이 있다고 판단되었습니다.
    따라서 더 확장하여 레이어링 해야한다는 생각하에 Route, Controller, Service, Model로 나누어 프로젝트 패턴을 완성했습니다. 각각의 레이어가 하나의 폴더이자 역할을 맡고 있습니다.

Layerd Pattern의 특징

  • 프로젝트 레이어링(알파벳 순서)
  1. Route → Controller → Service → Model로 갈 수록 데이터베이스 접근하는 로직에 근접하여 핵심 로직을 다루고 있습니다.
  2. 각각의 레이어는 오로지 바로 아래에 있는 레이어만을 의존하고 있습니다.(의존성)

6. 👬협업의 경험

- 협업을 진행하면서 사용했던 다양한 툴, DISCODE와 슬랙

  • 개발자로서 팀원들과 코드라는 매개체로 협업을 진행하면서 깨달았던 것들이 있었다. 이를 정리해보면 크게 세 가지 정도로 협업의 경험에 대해서 소개해보고싶다.
  1. 첫 째, 나만의 기준으로만 코드를 짜서는 안되고 모두가 함께 볼 수 있는 코드, 즉 가독성 좋고 명확한 코드를 만들어야한다.
  2. 둘 째, 코드를 잘 짜는 것 만큼 팀원들과 의사소통을 하는 과정 역시도 프로젝트를 진행하면서 중요하다는 사실을 깨달았다.
  3. 셋 째, 프로젝트를 진행하면서 내 코드를 보는 것만큼 다른 팀원들의 코드를 들여다보는 시간들을 가지게 되어 전체적인 프로젝트의 흐름뿐만 아니라 팀원들의 코드들을 스스로 해석하고 협업 과정에서 함께 수정할 수 있는 능력이 있어야한다.
  • 전반적으로 위 세 가지에 대해서 프젝트를 진행하면서 크게 와닿았던거같다. 이전까지는 혼자서 코드를 치고 보는데 익숙해져있었던 나였다. 그러나 협업의 과정을 밞아가며 한명의 팀원으로서 가져야하는 프로젝트에서 맡은 부분을 완수해야하는 책임감을 갖게되고 이 단어를 다시금 곱씹어 보는 계기가 되었다.
    뿐만 아니라 이러한 과정에서 다양하게 드러났던 부족했던 점과 잘했던 점들 역시 발견할 수 있는 좋은 추억이자 경험이 되었다.

7. 💯 아쉬운 점과 잘한 점

7_1> ❌ 아쉬운 점

  • 우리 팀의 경우 다양한 기능들 보다는 기본적인 것들에 충실하자는 의미에서 프론트와 백엔드 파운데이션에서 배웠던 과정들을 잘 적용한 사이트를 만들자는 목표를 세우고 프로젝트에 임했다. 그럼에도 불구하고 아무래도 프로젝트를 진행하는 과정에서 하지못했던 구현하지 못했던 추가 기능들(장바구니, 인가된 사용자가 진행할 수 있는 서비스 등)에 대한 아쉬움이 남는다.
    그러나 2차 프로젝트에서 이러한 아쉬움을 풀어보는 것을 다짐하며 아쉬움은 빨리 털어버리고 앞으로 나아가는 쪽을 택했다.

7_2> ⭕️ 잘한 점

  • 나름 프로젝트를 진행하는데 있어서 중요하게 여겼던 것이 팀의 분위기였던 만큼 최대한 팀원들과 함께 하는 시간을 효과적으로 보내며 효율적인 작업을 할 수 있는 분위기를 만드는데 나름의 노력들을 가하였고 실제로 결과로서 끝까지 좋은 분위기로 프로젝트를 마무리하는 데 있어서 일련의 보탬을 줬다는 생각에 있어 꽤 만족하고 있다.
  • 또한 이번 프로젝트 전체 기간, 2주에 거쳐 팀원들에게 하루를 돌이켜볼 수 있는 회고록을 작성해보자는 제안과 함께 회고 템플릿을 만들어서 공유 했었던 점을 꼽고싶다. 나뿐만 아니라 이 과정에서 팀원들 모두 바쁘게 돌아가는 프로젝트 내에서 자신이 하고 있는 작업들을 돌이켜볼 수 있는 시간을 가질 수 있었던거 같고 팀원들의 회고를 읽는 것으로 하루를 마무리하며 나 또한 프로젝트의 진행되는 흐름을 이해하는데 있어서도 큰 도움이 되었던거같다.

8. 🗂기억에 남는 코드 모음

8_1F> 📒 기억에 남는 프론트 코드

- productList/productList.js

changeMainToDetailImage = event => {
    const { detailImage } = this.props;
    const hoverEventArea = {
      hoverEventArea0: 0,
      hoverEventArea1: 1,
      hoverEventArea2: 2,
      hoverEventArea3: 3,
    };
    [
      'hoverEventArea0',
      'hoverEventArea1',
      'hoverEventArea2',
      'hoverEventArea3',
    ].forEach(area => {
      const idx = hoverEventArea[area];
      switch (event.target.className) {
        case `hoverEventArea${idx}`:
          this.setState({
            mainImage: detailImage[idx].detailImageUrl,
          });
          break;
        default:
      }
    });
  };

  • 호버되는 위치에 따라 다른 이미지를 보여주는 기능을 만드는 코드이다.
    이전의 코드는 switch문을 통해 분기를 주어 호버되는 위치의 태그의 클래스명과 일치하면 해당 태그에 따라디테일 이미지의 src 프로퍼티 값을 바꾸는 로직을 짰었었다.
  • 그러나 if문이나 switch문으로 코드를 짜게되면 가독성이 현저하게 떨어지며 중복된 요소들이 있어 객체로 맵핑이 가능하다는 피드백을 받게되었다.
  • 따라서 빈 div 태그 4개의 클래스명을 배열로 담아 이미지가 호버되는 위치에 따라서 forEach 메서드를 통해 클래스명으로 해당 배열의 인덱스를 담고 해당 객체의 인덱스에 따라 switch문의 조건을 하나로 묶게되니 중복을 줄일 수 도 있었고 코드 자체도 간결해지게 되어 기억에 남는 코드로 꼽고싶다.

8_2F> 📕 기억에 남는 프론트 코드

productList/productList.js

fetchMoreData = async () => {
    // 데이터를 fetch하는 함수
    const recent = this.state.sortOptions[0].isChecked;
    const pricehigh = this.state.sortOptions[1].isChecked;
    const pricelow = this.state.sortOptions[2].isChecked;
    const trend = this.state.sortOptions[3].isChecked;
    let queryParameter;
    recent && (queryParameter = 'recent');
    pricehigh && (queryParameter = 'pricehigh');
    pricelow && (queryParameter = 'pricelow');
    trend && (queryParameter = 'trend');
    const { hasMoreData, currentItem } = this.state;
    console.log(
      'Fetch 함수 실행 시 초기화된 currentItem(OFFSET)의 값',
      currentItem
    );
    // offset 0~20 LIMIT 10 sort = recent, pricehigh, pricelow, trend에 따라 페치 함수 실행
    fetch(`/product?offset=${currentItem - 10}&sort=${queryParameter}`)
      .then(res => res.json())
      .then(data => {
        const itemList = data.LIST_DATA.product;
        console.log(this.state.itemList, itemList);
        // 벡엔드에서 보내준 데이터 id 1번부터 10/ 11번부터 20씩/ 21번 부터 30번까지를 변수에 저장한다.
        const newItemList = [...this.state.itemList, ...itemList];
        // 스프레드 연산자를 활용해 배열을 누적해서 만든다.
        this.setState({
          itemList: newItemList,
        });
        // 스테이트 값을 누적된 배열 newItemList로 변경해준다.
        console.log(
          'itemList(추가된 데이터)',
          itemList,
          'this.state.itemList(전체 데이터)',
          this.state.itemList,
          '데이터 총길이',
          this.state.itemList.length
        );
        // 데이터 총 길이, 갯수가 페치된 갯수의 총합과 일치하면 (데이터 총합은 서버로부터 받도록 수정할 예정)
        const DATA_TOTAL_NUMBER = 30;
        // console.log(this.state.hasMoreData, this.state.isLoading);
        if (this.state.itemList.length === DATA_TOTAL_NUMBER) {
          // 더 이상 받을 데이터가 없도록 스테이트값 변경 하여 로딩 이미지 제거하기
          this.setState({
            hasMoreData: !hasMoreData,
          });
          // 스크롤 이벤트도 제거해주기
          return window.removeEventListener('scroll', this.handleScroll);
        }
      })
      .catch(console.error);
  };

  • 무한 스크롤 기능을 구현하기 위한 메서드 fetchMoreData와 관련된 코드이다.
    이전에 벡엔드와 작업을 하기 전에 정렬 기능이 없는 상태로 무한 스크롤을 구현하다보니 데이터 개수를 그대로
  • 마찬가지로 벡엔드에 정렬기능이 추가된 형태의 리스트를 가져와야하다 보니 비동기로 동작하고 있는 setState를 동기적으로 만들기위해 2가지 방식, setState의 2번째 인자로 콜백함수로 전달하거나 componentDidUpdate를 이용한 방식이 있음을 알게되었고 첫번재 방식을 활용해서 동기적으로 currentItem, offset값을 초기화해줌으로서 무한 스크롤을 구현하였다.
  • 이 코드가 기억에 남았던 건 목데이터 json 형태로 무한 스크롤 기능을 혼자 구현을 하다가 벡엔드 API를 붙이는 과정에서 많은 배운 점들이 있었기 때문이다. 우선 프론트와 벡앤드를 함께 다루면서 소통하는 시간이 짦다보니 실제 목데이터의 형태를 맞추지도 어떠한 형태로 서로 구현을 해야할지 맞춰보는 점이 부족했던것 같다. 이 코드를 보면 앞으로 특히 백엔드 API가 필요한 코드를 맡게된다면 더 자신있게 코드를 짤 수 있다고 말 할 수 있다.

8_3B> 📗기억에 남는 백앤드 코드

- app.js

app.use(routes);
app.use((req, res, next) => {
  next(new AppError.notFoundError(`NOT FOUND ${req.originalUrl}`));
});
app.use(errorHandler);

-전역에서 에러를 잡기 위해서 라우트에서 발견하지 못한 경로에 대한 404에러를 잡아준다.
-그 외의 모든 에러는 errorHandler 미들웨어를 통해 next로 넘겨받아 app.js에서 최종적으로 에러를 잡을 수 있다.

- errors/appError.js

class AppError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? `FAILED` : 'ERROR';
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
  static keyError(message) {
    return new AppError(400, message);
  }
  static valueOfKeyError(message) {
    return new AppError(400, message);
  }
  static invalidError(message) {
    return new AppError(403, message);
  }
  static checkPolicyAgreeError(message) {
    return new AppError(403, message);
  }
  static checkAuth(message) {
    return new AppError(403, message);
  }
  static checkJWTAuth(message) {
    return new AppError(403, message);
  }
  static notFoundError(message) {
    return new AppError(404, message);
  }
  static duplicatedError(message) {
    return new AppError(409, message);
  }
}
module.exports = AppError;
  • 에러 디렉토리안에 있는 appError.js 파일로 클래스 형태로 new AppError 객체를 리턴해줌으로서 에러를 에러 코드 번호와 메세지에 맞게 코스튬해서 던져주도록 짜보았다.
  • new Error를 클래스 형태로 커스튬한 뒤 next로 넘겨받아
  • 코스튬해서 쓰는 점이 편리하고 제가 보았을 때는 깔끔하다고 생각이 되지만 팀원들이 보았을 땐 내용이 길고 방대해서 더 가독성 좋고 보다 효과적으로 코드를 정리할 수 있는 방식에 대해서 더 알아봐야겠다는 생각에 꼽아보았다.
  • utils/catchAsync.js
module.exports = (fn) => {
  return async (req, res, next) => {
    try {
      await fn(req, res, next);
    } catch (error) {
      next(error);
    }
  };
};
  • 랩퍼함수를 만들어서 컨트롤러 함수를 감싸줘 에러를 전역에서 넘겨받을 수 있도록 했다.
  • 저렇게 내부에 다시 try catch로 감싼 건 비동기에러 뿐아니라 동기적인 에러처리도 처리해주시기 위해서이다.

- middleware/errorHandler.js

import AppError from '../errors/appError';
function errorHandler(err, req, res, next) {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';
  if (err instanceof AppError) {
    console.error(err.stack);
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message,
    });
  } else {
    console.error(err.stack);
    res.status(err.statusCode).json({
      status: 500,
      message: "INTERNET ERROR"
    });
  }
}
module.exports = errorHandler;
  • app.js에서 던져진 error가 AppError 객체의 프로퍼티에 속하면 넘겨받은 상태값과 메세지를 던져지도록 하고 그 외의 에러는 err.stack과 함께
    500에러, INTERNET ERROR를 던져주도록 만들었다.

8_4B> 📘 기억에 남는 백앤드 코드

-util/validate.js

import AppError from '../errors/appError';
export const validation = (userInfo, KeyList) = {
  const { email, password, name, address } = userInfo;
  const essentialInfo = { email, password, name, address };
  const keys = Object.keys(userInfo);
  // undefined, null, ''(빈스트링) 모두 잡을 수 있도록 essentialInfo[key]가 false인 경우 반환하도록
  const emptyInfoValue = keys.filter((key) => !essentialInfo[key]);
  const emptyInfoKey = KeyList.filter((key) => keys.indexOf(key) === -1);
  if (emptyInfoKey.length !== 0) {
    return new AppError.keyError(`${emptyInfoKey}_KEY_EMPTY`);
  }
  if (emptyInfoValue.length !== 0) {
    return new AppError.valueOfKeyError(`${emptyInfoValue}_VALUE_EMPTY`);
  }
};
  • 리팩토링을 하면서 많은 수정을 하게된 코드이다.
  • 기존에는 키에러와 키의 빈값에 대한 에러처리를 해주는 함수를 따로 만들었는데 멘토님께서 하나의 validate함수로 묶을 수 있다는 조언을 듣고 반영해보았다.
  • 기존의 코드에선 map이 가진 메서드 entries와 keys을 써서 구현을 하였으나 자료구조에 있어서 아직 충분한 숙달이 되지 않았다는 판단하에 Object에도 keys라는 메서드가 있어서 객체의 키와 값을 조회하는 방식으로 구현해보았다.
  • 요청에 대한 응답의 결과로 빈 키와 값을 모두 전달해주니 기존의 하나 씩만 알려주는 코드보다 더 효과적이라는 생각도 든다.

번외) 🧑‍💼 내가~만약~면접관이라면~

Q❓) 프론트 백엔드 모두 공부를 하였는데 왜 하나를 선택하지 않고 둘다 공부를 하게 되었는지 ? 그 계기가 무엇인지 ?
A❗️) 개발을 하는데 있어서 프론트 백엔드 구분없이 전체를 알아야하는 상황이 언제든지 올거라고 생각한다. 이를 대비하기 위해서 둘다 놓칠 수 없다는 판단을 하게 되었고 스스로에게도 한계를 두지 않고 다양한 것들 경험하기 위해서 풀스텍 과정을 선택을 하게 되었다.

Q⁉️) 프로젝트를 진행하면서 스스로 잘했다고 생각한 점과 아쉬운 점이 있는지?
A‼️) 잘한점 - 프로젝트에 들어가기에 앞서 팀원들과 성장하자는 의미에서 지금까지 써오던 회고록을 수정해서 프로젝트 과정 하루하루를 돌이켜볼 수 있는 회고 다이어리 템플릿을 만들어 매일 작성하고 팀원들과 공유하는 시간을 가져왔었는데 굉장히 좋았던 거같다.
아쉬운 점 - 프로젝트를 진행하면서 구현해보고싶었던 것과 더 공부하여 리팩토링하고 싶은 것들이 차고 넘쳤는데 완성도를 높이기 위해서 이를 포기했음에도 불구하고 욕심이 났던 점들이 오히려 프로젝트 집중에 어려움을 만들기 도했다. 지금 당장 할 수 있는 것과 하고싶은것은 구분해야한다는 점에 있어서 아쉬웠던거같다


Reference) 이 프로젝트는 Thisisneverthat 사이트를 참조하여 학습목적으로 만들었습니다. 실무수준의 프로젝트이지만 학습용으로 만들었기 때문에 이 코드를 활용하여 이득을 취하거나 무단 배포할 경우 법적으로 문제될 수 있습니다. 이 프로젝트에서 사용하고 있는 사진 대부분은 위코드에서 구매한 것이므로 해당 프로젝트 외부인이 사용할 수 없습니다. © 2021 GitHub, Inc. Terms Privacy Security Status Docs Contact GitHub Pricing API Training Blog About

profile
자기 신뢰의 힘을 믿고 실천하는 개발자가 되고자합니다.

2개의 댓글

comment-user-thumbnail
2021년 10월 21일

역싀,, 민재님의 열정..! '책임감'의 무게에 공감하고 갑니다 하핳

답글 달기
comment-user-thumbnail
2021년 10월 23일

미민! 항상 많이 배우고 있습니다. 프로젝트 함께해서 즐거웠습니다! 파이팅^^!

답글 달기