소감 & 회고록

[회고] 첫 프로젝트: 4bsop - Aesop을 모델링한 커머셜 웹사이트 제작

왈왈디 2023. 4. 17. 01:30
728x90

📍 생애 첫 프로젝트

2023년 4월 3일부터 4월 13일까지12일 간 위코드 부트캠프에서 생애 첫 프로젝트를 진행하게 되었다.

참여 인원은 프론트엔드 2명과, 백엔드 2명(나 포함).

 

동기들은 5명, 6명으로 팀이 구성되어, 팀원이 4명인 팀은 우리 뿐이라, 시작 전에는 다른 팀들에 비해 뒤쳐지거나 완성도가 비교될까  걱정이 되었었다. 캠프 멘토님들도 커머셜 웹사이트의 기본 구성요소인 [회원가입, 로그인, 상품 목록, 상품 상세 페이지, 장바구니, 주문, 결제] 중 '장바구니'까지 구현하는 것을 목표로 하라고 조언을 주셨었다.

 

그러나 결국 12일의 시간 동안 회원가입부터 결제까지 모든 기능을 백엔드와 프론트엔드 통신을 통해 문제 없이 진행되도록 만들어냈다.

더 많은 기능을 구현했다고 해서 더 좋은 프로젝트 결과물이 되는 것은 아니지만, 프론트엔드에서 mock data를 사용하는 등의 눈속임 없이 모든 기능을 백엔드와의 통신으로 구현하고, 모두에게 첫 프로젝트임에도 팀 전체가 함께 목표치 이상을 달성했다는 사실이 매우 뿌듯했다.

 

📍 Agile 업무 방식에 대한 경험

경영학도로서 Agile에 대해서는 익히 알고 있었고, 매우 효율적인 업무 방식이라는 생각에 선망하는 마음도 있었다. 이전 직장에서도 개발은 아니지만 프로젝트 형식의 업무가 진행되어 Agile한 업무 방식을 도입해보고 싶었으나, 신입으로서 업무 방식을 결정할 수 있는 권한이 크지 않아 어려웠다. 이번 프로젝트에서 처음 Agile 업무 방식 중 Scrum 방식을 경험해보게 되었다. Scrum의 핵심은 '빠르게 만들고, 그걸 고치고 덧붙이며 나아간다'인 것 같다.

[Agile 개발 방식의 예시]

Scrum 방식의 업무를 구현하기 위한 Sprint라는 개념은 이번에 처음 알게 되었는데, 우리는 1주차와 2주차 각각 두 번의 sprint를 진행했다. 기능을 구현한 후 완성도를 높이는 것이 우리의 목표였고, 첫 번째 sprint에서 전체 목표를 '장바구니 기능 구현'까지로 잡고 그 2/3 정도를 첫 sprint의 달성 목표로 잡았다. 그러나 첫 주에 이미 장바구니 기능 구현을 완료하고도 일정이 널널한 상황이 되어, 2차 sprint planning 미팅에서 주문과 결제까지 구현하는 것으로 목표를 변경하게 된다. 2차 sprint에서도 계획했던 기능에 대한 구현은 시간 내에 널널하게 완료하였다. 남은 시간에는 프론트엔드와 백엔드 간의 통신에 집중했고, 통신 과정에서 수정할 부분이 꽤나 있었기에 여유롭게 통신을 진행한 것이 좋은 판단이었다는 생각이 들었다. 또, 통신하며 프론트엔드가 코드를 수정할 동안에는 백엔드에서도 더 나은 효율적인 코드가 될 수 있도록 틈틈이 리팩토링을 했다.

[1차 Sprint 회고 미팅 회의록 - Team Notion으로 작성함]

 

sprint를 진행하며 한 가지 아쉬웟던 점은, 1차 Sprint 이후 달성 내역을 리뷰할 당시 새로 작성한 목표에 대한 달성률만을 확인하였는데, 초기 설정 목표에 대한 1차 Sprint의 달성률도 함께 점검하여 sprint 간 구분을 명확히 하고, 팀이 초과 달성했다는 사실을 알려 팀원들의 사기를 높였다면 더 좋았을 것 같다.

 

전체적인 개발 과정에 있어서, 기본적인 틀을 완성한 후 디테일을 다듬고 기능을 추가하니 일이 순조롭게 흘러갔고, 뒤로 돌아가야하는 일이 없었다. 전 직장에서 발표 자료용 ppt를 시각화까지 예쁘게 완성하여 윗분들께 보여드리고, 까이면 기획부터 다시 시작하던 경험이 숱한 나로서는 Agile 방식의 매력을 제대로 느낄 수 있는 경험이었다. 

 

📍 Project Manager로서 역할 수행

각 팀에는 Product Manager와 Project Manager라는 직책이 존재했고, 나는 그 중 Project Manager를 맡았다. Product Manager는 우리가 개발하는 상품에 대한 기획, 그리고 개발 과정에서 우리가 기획한 대로 상품이 잘 완성되어 가는지 관리하는 역할이었고, 내가 맡은 Project Manager는 매일 팀원들의 진행상황을 확인하고 sprint별 목표 달성률을 확인하여 목표를 조정하는 등 기술적인 목표 설정과 달성을 책임지는 역할이었다. 목표 의식이 뚜렷하고, 적극적으로 의견을 내는 것을 선호하는 편이라 Project manager(이하 PM)를 맡아 팀의 효율적인 업무 진행에 일조하고 싶어 자원했는데, 팀원들이 기꺼이 동의해준 덕에 그 역할을 맡을 수 있었다.

 

실질적으로  PM으로서 맡아 진행한 대표적인 일 중 하나는 Trello로 우리 팀이 달성해야할 과업을 하나의 수행 단위로 쪼개어 티켓을 만들고 그것을 [진행 예정, 진행 중, 리뷰 받는 중, 완료] 등 적절한 상태로 업데이트 시키는 일이었고, 또 하나는 매일 아침 Daily Standup Meeting(DSM)을 주관하는 일이었다. 

 

Trello 티켓은 업무를 작은 단위로 쪼개어 이틀 단위로 담당자를 할당하고 업무 진행 속도를 보며 다음 업무 담당자를 지정하는 식으로 진행했다. 업무의 우선순위는 기초를 완성하고 살을 붙인다는 원칙에 근거하여 정했고, 작은 규모의 프로젝트였기에 큰 이견은 없었다. 사실 내가 속해있는 백엔드의 업무 티켓 관리에는 큰 어려움이 없었으나, 상대적으로 나의 이해도가 낮은 프론트엔드의 티켓 관리가 쉽지 않았다.

[프로젝트 마지막날의 Trello]

첫 티켓 생성 때 프론트엔드의 티켓을 페이지 단위로 만들었었는데, 프론트엔드는 레이아웃과 기능 구현이 다소 분리된다는 사실을 1차 sprint가 끝나갈 때쯤 알게 되었다. 그 이후에는 페이지별로 레이아웃과 기능구현 티켓을 따로 만들었다. 또 Github repository를 따로 운영하다 보니, 프론트엔드 코드가 어디까지 작성되었고, 현재 Main branch에는 어떤 기능들이 merge되었는지 등 확인하는 것에 소홀해졌던 것 같다. DSM에서 프론트 팀원들이 코드 작성은 완료했으나 merge는 아직 안됐다고 여러번 이야기했었는데, 좀 더 명확하게 이유를 묻고 Trello 티켓도 상태 업데이트를 하며 구체적인 사항들이 팀 전체에 함께 공유될 수 있도록 했으면 더 좋았을 것 같다. 당시에 프론트엔드가 git 사용에 어려움을 겪어 branch를 활용하지 못하고 하나의 branch에서 여러 기능들을 작업하여, 거의 모든 작업을 한 branch에 작성하고 있었던 것이었다. 미리 알았더라면 관련해서 함께 문제를 해결할 수 있게 도움을 줄 수 있었을 것 같은데 그러지 못한 부분이 많이 아쉬웠다.

 

DSM은 매일 아침 학원에 나와 10분 정도 개인 정비 시간을 갖고, 바로 진행했다. 처음 며칠은 정해진 형식 없이 다소 자유롭게 현재 작업 중인 것들과 공유되어야 할 부분들을 이야기했는데, 중간에 멘토님의 피드백을 받고, [어제 까지 한 일, 오늘 할 일, 내일 할 일, 겪고 있는 어려움(Blocker)] 네 가지 형식에 맞추어 진행하는 것으로 방향을 잡았다. 피드백 덕분에 훨씬 체계적인 DSM을 진행할 수 있었고, DSM을 진행하는 것 자체가 매우 좋은 방식이라는 생각이 들었다. 프론트엔드 백엔드가 분리되어 있더라도 함께 어려운 점을 공유하면 합심하여 해결할 수 있는 일들이 많았다. 

 

 

여기까지가 프로젝트를 진행하며, 팀원이자 PM으로서 느꼈던 점이다.

아래에서는 백엔드 개발자로서 데이터베이스 구축과 API 작성 중 느낀 점들을 적어보겠다.


📍 ERD

백엔드로서 초기 세팅 이후 가장 먼저 진행한 작업은 ERD 설계였다. 커머셜 웹사이트인 만큼 poroducts와 users를 중심으로  설계를 해 나갔다. 

[초기 설계 ERD]

작업을 진행하며, 프론트에서 화면 구성에 필요한 속성을 테이블에 추가하고, 각종 에러를 방지하기 위해 Constraint를 추가하는 등의 수정이 이루어졌다. 초기 세팅한 테이블을 마이너하게 수정하는 것이 여러 작업을 수반하므로 망설여지기도 했으나, 더 나은 결과물을 위해 수정해나가는 것이 agile한 개발 기조와도 잘 맞다고 판단했다. 과감히 수정하고 백엔드 팀원끼리 git에 dbmate로 변경사항을 올려 테이블 구조를 통일시켰다. 되돌아보니 전체적인 효율성을 보아 필요할 때마다 바로 ERD를 수정한 것이 잘한 선택이었던 것 같다.

 

📍 핵심 트러블 슈팅

1. 상품 조회 API - productDao 쿼리문 효율화

내가 맡은 부분 중 상품리스트와 상품 상세 페이지 구현을 위해 상위 카테고리, 하위 카테고리, 정렬 조건, 페이지네이션, 상품 id에 따라 상품 정보를 보내는 API 구현이 있었다. 상품 정보를 전송하기 위해서 Database에서 SELECT query문으로 정보를 불러오는 과정이 필요했다. 처음에는 각 조건에 따라 데이터를 불러오는 query문이 포함된 function을 productDao 내에 각각 작성했다. 그러나 세번 째 같은 SELECT문에 조건만 다르게 붙은 함수를 작성할 때 즈음, 하나의 함수로 이 기능을 구현할 수는 없을까 하는 의구심이 들었다.

 

각각의 함수를 처음에는 if문으로 상/하위 카테고리 id, 상품 id 값이 인자로 들어오면 그에 맞게 query문 조건이 실행되도록 하나의 함수로 코드를 작성했다. 

//productDao.js - if문을 활용한 getProductByCondition 함수

const getProductsByCondition = async (subId, mainId, pId, isMain) => {
  try {
    let condition = '';
    if (subId) {
      condition = `WHERE sc.id = ${subId}`;
    } else if (mainId) {
      condition = `WHERE m.id = ${mainId}`;
    } else if (pId) {
      condition = `WHERE p.id = ${pId}`;
    } else if (isMain) {
      condition = `WHERE p.main_product = ${isMain}`;
    }

    return await appDataSource.query(
      `SELECT 
        p.id,
        p.name,
        p.price,
        p.description,
        p.size_id sizeId,
        p.sub_category_id subCategoryId,
        s.size size,
        sc.name subCategoryName,
        m.id mainCategoryId,
        m.name mainCategoryName,
        i.url imageUrl,
        joined_ig.ig_array ingredients
    FROM products p
    JOIN sizes s ON p.size_id = s.id
    JOIN sub_categories sc ON sc.id = p.sub_category_id
    JOIN main_categories m ON sc.main_category_id = m.id
    JOIN products_images pi ON p.id = pi.product_id
    JOIN images i ON i.id = pi.image_id
    JOIN (
        SELECT
            pig.product_id pid,
            JSON_ARRAYAGG(ig.name) ig_array
        FROM ingredients ig
        JOIN products_ingredients pig ON pig.ingredient_id = ig.id
        GROUP BY pig.product_id
    ) joined_ig ON joined_ig.pid = p.id        
    ${condition}`
    );
  } catch (err) {
    err.message = 'DATABASE_ERROR';
    err.statusCode = 400;
    throw err;
  }
};

그러나 이 방식도 정렬, 페이지네이션 등이 추가될 경우 여러 조건이 동시에 적용되기 어렵다는 문제에 부딪혔다. 그 문제를 해결하기 위해 단축 평가를 활용한 함수를 만들었다.

//productDao.js - 단축 평가를 활용한 getProductsByCondition 함수

const getProductsByCondition = async (
  subId,
  mainId,
  pId,
  isMain,
  orderBy,
  sorting,
  offset = 0,
  limit = 10
) => {
  try {
    const conditions = [
      subId && `WHERE sc.id = ${subId}`,
      mainId && `WHERE m.id = ${mainId}`,
      pId && `WHERE p.id = ${pId}`,
      isMain && `WHERE p.main_product = ${isMain}`,
    ].filter(Boolean);

    const orderings = [
      orderBy && `ORDER BY ${orderBy}`,
      sorting && `${sorting}`,
    ].filter(Boolean);

    const pagination = [
      limit && `LIMIT ${limit}`,
      offset && `OFFSET ${offset}`,
    ].filter(Boolean);

    const condition = conditions[0] || '';
    const ordering = orderings.join(' ') || '';
    const paging = pagination.join(' ') || '';

    return await appDataSource.query(
      `SELECT 
        p.id,
        p.name,
        p.price,
        p.description,
        p.summary,
        p.size_id sizeId,
        p.sub_category_id subCategoryId,
        s.size size,
        sc.name subCategoryName,
        m.id mainCategoryId,
        m.name mainCategoryName,
        i.url imageUrl,
        joined_ig.ig_array ingredients
    FROM products p
    LEFT JOIN sizes s ON p.size_id = s.id
    LEFT JOIN sub_categories sc ON sc.id = p.sub_category_id
    LEFT JOIN main_categories m ON sc.main_category_id = m.id
    LEFT JOIN products_images pi ON p.id = pi.product_id
    LEFT JOIN images i ON i.id = pi.image_id
    LEFT JOIN (
        SELECT
            pig.product_id pid,
            JSON_ARRAYAGG(ig.name) ig_array
        FROM ingredients ig
        JOIN products_ingredients pig ON pig.ingredient_id = ig.id
        GROUP BY pig.product_id
    ) joined_ig ON joined_ig.pid = p.id        
    ${condition}
    ${ordering}
    ${paging}`
    );
  } catch (err) {
    err.message = 'DATABASE_ERROR';
    err.statusCode = 400;
    throw err;
  }
};

여러 조건들이 다양한 형태로 들어오더라도 모두 처리 가능한 하나의 함수를 구현하여 뿌듯했다. 다만, 이 경우 orderBy와 sorting, offset과 limit이 반드시 pair로 들어와야하는데 이 경우를 커버하기 위해 productService 함수 내에 관련 에러 핸들러를 작성했다.

//productService.js - 반드시 함께 들어와야 하는 인자에 대한 에러처리 if문

  if (!orderBy !== !sorting) {
    const err = new Error('CONDITION_NEEDS_TO_BE_PAIR');
    err.statusCode = 400;
    throw err;
  }

  if (!offset !== !limit) {
    const err = new Error('CONDITION_NEEDS_TO_BE_PAIR');
    err.statusCode = 400;
    throw err;
  }

Dao query문 함수는 더 짧고 효율적으로 작성할 수 있는 여지가 큰 것 같아 추후에 리팩토링 해보고 싶다.


2. 결제 API - 잔액 부족 에러 해결

내가 맡은 결제 API를 프론트엔드와 통신하여 구동하던 중, user의 보유 point가 충분함에도 잔액 부족 Error가 발생하는 상황이 생겼다. 분명 잔액 point가 충분함을 확인했고, 코드를 아무리 쳐다봐도 왜 error가 나는지 이해할 수 없었는데, console.log()로 데이터 타입을 확인해보니 문제를 알 수 있었다.

//orderService.js - 잔액이 충분해도 잔액 부족 에러가 났던 기존 코드

if (user.point < order.totalPrice) {
    const err = new Error('INSUFFICIENT_POINT');
    err.statusCode = 400;
    throw err;
};

user.point와 order.totalPrice가 string 타입으로 인식되어 user.point가 [10,000,000] 있더라도 order.totalPrice가 [900,000]이라면 if문이 동작하여 에러로 인식되는 것이다. 둘을 number 타입으로 인식시키기 위해 아래와 같이 코드를 수정했다.

//orderService.js - 숫자로 인식되도록 수정한 코드
  
if (user.point - order.totalPrice < 0) {
    const err = new Error('INSUFFICIENT_POINT');
    err.statusCode = 400;
    throw err;
}

프로젝트 저장소 링크: https://github.com/wecode-bootcamp-korea/44-1st-four-branch-backend

 

GitHub - wecode-bootcamp-korea/44-1st-four-branch-backend

Contribute to wecode-bootcamp-korea/44-1st-four-branch-backend development by creating an account on GitHub.

github.com

다음 프로젝트에서는 이번 프로젝트에서 배운 점들을 적극 반영해서 더 좋은 코드를 작성해야지.

728x90