땅땅마켓(TTMK) 개발로그

flobeeee·2021년 4월 27일
0

프로젝트

목록 보기
4/7

⚝ 프로젝트 기간

2021.03.30 ~ 04.25 (약 1달)
팀원 4명 (SNB 랑 같은 멤버다)
나는 백엔드를 맡았고, 서버가 모두 구축된 뒤에 프론트를 도왔다.

⚝ 기획

  • 프로젝트 아이디어 선정
    땅땅마켓
    지역기반 중고물품 거래
    경매 시스템
    실시간 금액 변동
    실시간 채팅

  • Team
    팀 이름: Lilakchal 라일락찰
    프로젝트 이름: 땅땅마켓 (ttangttang market)

  • 주요 기능

    • 위치기반 (카카오 API 활용)
    • 로그인 (카카오 OAuth)
    • 검색페이지
    • 실시간 입찰 (socket.io)
    • 마이페이지 (필터버튼으로 판매, 입찰 아이템 확인)
    • 실시간 채팅 (socket.io)
    • 무한스크롤
  • 도메인
    ttangttang.shop
    도메인에 땅땅을 넣고 mall의 줄임말 같은 ml 을 붙였다.
    (무료도메인을 사용했는데, 접속이 안되는 경우가 있어서 .shop 로 변경했다.)

  • 깃허브 레포
    서버 ( readme, wiki )
    클라이언트

  • 스키마

  • 스택

⚝ 개발과정

⚝ 서버

  1. HTTPS 구현
    https 서버를 구축해서 테스트까지 해보고,
    추후 배포를 위한 EC2 구동 코드도 주석처리로 준비해뒀다.
    이번에도 노드몬을 설치하여 편한 개발환경을 만들었다.

  2. MySQL 데이터베이스 구축
    Sequelize 를 활용하여 DB를 구축하고 각 model을 만들어 node와 연결했다.
    조인테이블이 들어간 관계형 DB를 처음부터 혼자 만들어보는 건 처음이라서
    3일정도 삽질할 생각이었는데, 생각보다 잘 만들어져서 하루만에 구축했다.
    DB쿼리 테스트를 할 수 있는 파일도 만들었는데, 조인테이블 연결에 시간을 많이 쏟았다.
    DB 셋팅을 하다보면 models 폴더에 index.js 가 생기는데, 여기서 관계설정을 했다.

    Chat 테이블은 조인도 되어있고 채팅내용도 담겨있어서 belongsTohasMany 로 관계설정을 해줬다. 이런식으로 해야 쿼리문을 날릴 때 채팅내역을 가져올 수 있었다..

// models/index.js

const { User, Item, Buyer_item, Seller_item, Chat } = sequelize.models;

Chat.belongsTo(User);
User.hasMany(Chat);

Chat.belongsTo(Item);
Item.hasMany(Chat);

User.belongsToMany(Item, { through: 'Seller_item', foreignKey: 'UserId', as: 'Item' });
User.belongsToMany(Item, { through: 'Buyer_item', foreignKey: 'UserId', as: 'ItemB' });

Item.belongsToMany(User, { through: 'Seller_item', foreignKey: 'ItemId', as: 'User' });
Item.belongsToMany(User, { through: 'Buyer_item', foreignKey: 'ItemId', as: 'UserB' });

Buyer_item.belongsTo(User, {
  foreignKey: 'UserId',
  as: 'UserB'
});

Buyer_item.belongsTo(Item, {
  foreignKey: 'ItemId',
  as: 'ItemB'
});

Seller_item.belongsTo(User, {
  foreignKey: 'UserId',
  as: 'User'
});

Seller_item.belongsTo(Item, {
  foreignKey: 'ItemId',
  as: 'Item'
});

추가로 클라이언트에서 테스트 할 수 있게 테이블마다 시드를 넣어줬다.

  1. 카카오 소셜로그인
    네이버API와 카카오API 중 어떤 것을 이용할까 고민했다.
    위치기반서비스도 구현해야해서 위치API도 함께 제공하는 것을 원했다.
    네이버는 위치기반서비스가 유료화로 전환이 되어있어서 자연스럽게 카카오를 활용했다.

    개인적으로 소설로그인을 할 때, 내 정보가 유출되지 않을까 걱정한 경험이 있다.

    그래서 최소한의 정보를 이용했다. 카카오이메일 아이디를 사용하지 않고도 유저 개개인마다 일련번호가 있어서 해당 번호를 DB에 저장했다.
    추가로 가져오는 정보는 카카오 닉네임 뿐이었다.
    땅땅마켓 내에서 사용자 이름을 표시하기 위해서 사용했고, 이것도 싫다면 닉네임 변경으로 쉽게 바꿀 수 있게 구현했다.
    아래 코드는 카카오 소셜로그인에 사용했던 코드이다.
    passpost 라이브러리를 활용할까 하다가, 어려운 길을 먼저 가보고 싶어서 도전했다.
    다 구현하고 나서 정말 뿌듯했다.

require('dotenv').config();
const axios = require('axios');
const qs = require('qs');
const { User: UserModel, Item: ItemModel } = require('../models');
const clientID = process.env.KAKAO_CLIENT_ID;
const clientSecret = process.env.KAKAO_CLIENT_SECRET;
const redirectURL = process.env.KAKAO_REDIRECT_URL;


module.exports = {
// 카카오 오어스 로그인, 강제회원가입
  'oauth': async (req, res) => {
    const { authorizationCode } = req.body;
    // console.log('1. 클라이언트에서 코드 들어옴', authorizationCode);
    axios({
      method: 'post',
      url: 'https://kauth.kakao.com/oauth/token',
      headers: {
        'Content-type': 'application/x-www-form-urlencoded;charset=utf-8',
      },
      data: qs.stringify({
        grant_type: 'authorization_code',
        client_id: clientID,
        redirect_uri: redirectURL,
        code: authorizationCode,
        client_secret: clientSecret,
      })
    }).then((response) => {
      const accessToken = response.data.access_token;
      // console.log('2. 토큰받음', response.data);
      axios.get('https://kapi.kakao.com/v2/user/me', {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'content-type': 'application/x-www-form-urlencoded',
        },
      })
        .then((data) => {
          // console.log('3. 유저정보받음', data.data);
          const kakaoid = data.data.id;
          const name = data.data.properties.nickname;
          UserModel
            .findOrCreate({
              where: {
                kakaoid: `${kakaoid}@kakao.com`,
              },
              defaults: {
                name: name,
              },
            }).then((user) => {
              // console.log('4. 회원가입', user);
              const { kakaoId, name, id } = user[0].dataValues;
              res.set('Set-Cookie', [`accessToken=${accessToken}`]);
              res.status(200).json({ kakaoId, name, id });
            });
        }).catch(e => {
          console.log('에러', e);
          res.status(500).json({ 'message': 'Fail to login' });
        });
    }).catch(e => {
      console.log('에러', e);
      res.status(500).json({ 'message': 'Fail to login' });
    });
  },
}
  1. API 구현
    팀원들과 함께 만든 API문서를 보고, 차례대로 만들었다.
    정해진 요청을 받아서 DB에서 정보를 가져와 내보내주는 작업은 할 때마다 재미있다.
    성격 상 남들이 필요한 것을 찾아주는 걸 좋아하는데,
    API 구현은 내가 좋아할 수 밖에 없는 일이었다.
    sequelize 가 아직 익숙하지 않았는데, 많이 공부할 수 있었던 시간이었다.

  2. sequelize 관련 이슈
    좀 심각한 이슈는 프로젝트 진행 중에 기록을 남겨놨었다.
    문제발생 상황을 기록하고, 시도한 방법과 해결방법을 작성해놓았다.

  • createdAt 삭제했는데 쿼리문 날릴 때마다 값 달라고 에러뜸
    첫 번째 경우, createdAt 컬럼이 없는 경우에도 다른 컬럼의 속성이 Date 면 해당 컬럼을 요구했었다. 테이블마다 해당컬럼이 필요없다고 설정을 해줬어야 했다.
  • N:M limit, offset 가 가능하다 ?
    두 번째 경우, N:M 관계의 경우 limit, offset 가 불가능하다는 글이 꽤 많았다.
    그래서 N:M을 바로 설정하지 않고 각각 조인테이블에 1:N 관계설정으로 변경을 해야하나 고민하면서 계속 구글링을 하던 와중에 비공식적인 방법을 찾아내 설정하였다.
    현재 배포까지 완료한 후에도 딱히 문제는 발생하지 않았다.
  1. 아이템 시드 마감일자 변경
    처음에는 해당 일자를 수기입력해서 대부분의 아이템들이 경매마감이 되면 해당 명령어를 쓰고 시드 날짜를 또 변경해서 업데이트 해줬다.
    npx sequelize-cli db:seed:undo:all
    npx sequelize-cli db:migrate:undo:all
    npx sequelize-cli db:migrate
    npx sequelize-cli db:seed:all
    이 방식이 굉장히 귀찮아서 마이그레이션을 업데이트하는 기준일보다 하루 뒤, 이틀 뒤 등등 자동으로 설정되게 변경하였다.
    아래는 변경한 코드이다.
endTime: new Date() // 이 옵션은 바로 마감을 시키는 시간이다
endTime: new Date(new Date().setDate(new Date().getDate() + 1)) // 1일 뒤
endTime: new Date(new Date().setDate(new Date().getDate() + 2)) // 2일 뒤
endTime: new Date(new Date().setDate(new Date().getDate() + 10)) // 10일 뒤

⚝ 클라이언트

  1. 무한스크롤 (Infinite Scroll)
    이번 프로젝트에서 내가 제일 사랑하는 기능이다.

    영상보러가기
    구현하면서 시행착오에 대해 영상으로 정리해봤다. 내 첫 유튜브 영상이다.
    처음에는 스크롤이 화면끝에 닿으면 서버에 요청하는 걸로 구현했다가,
    스크롤이 끊기는 느낌이 들어서 화면끝에 닿기전에 요청을 보내는 걸로 수정했다.

    조건에 변수를 추가해서 제어했다.

let oneTime = false; // 무한스크롤시 중복요청 방지

useEffect(() => {
    return () => {
      window.onscroll = null; // 다른페이지에 영향안주에 처리
      window.scrollTo(0, 0);
    };
  }, []);

  window.onscroll = function() {
    //window height + window scrollY 값이 document height보다 클 경우,
    if((window.innerHeight + window.scrollY) 
       >= document.body.offsetHeight * 0.8 && !oneTime) {
      oneTime = true; // 중복요청하지 않게 조건변경
      setCount(Count + 6); // offset 옵션
      requestSearchItems({ // 서버에 요청하는 함수
        params: { city: city, offset: Count, keyword: match.params.keyword }},
        requestCallbackByScroll);
    }
  };
  1. 로딩페이지 노출 로직구현
  • 페이지에서 서치페이지로 넘어가는 순간에 위치정보에 대한 동의유무를 받는다.
    동의하면 지역을 받아오고, 비동의하면 '전국'으로 지역을 표기했다.
    지역정보가 존재하지 않으면 로딩화면의 띄우게 했다.

  • 검색페이지와 마이페이지로 이동할 때, 아이템들의 잔상이 남았다.
    예를들어 검색페이지에서 마이페이지로 이동하면 마이페이지 화면에서 검색페이지의 아이템이 초반에 보였다가 새로 업데이트 되는 현상이 일어났다.
    랜더링에 조건문을 넣어서 해결하였다.
    페이지를 나갈 때 특정 변수를 false 로 변경하고, 이동한 페이지도 false 로 설정한 후,
    아이템이 업데이트 되면 true로 변경해 그 후에 화면이 렌더링이 되게 했다.

  1. 채팅페이지 화면비율 및 반응형

    css에서 제일 어려운 게 비율맞추는 것 같다.
    flex를 애용하긴 하지만, 할 때마다 마음대로 안된다.
    미디어 쿼리도 처음 사용해봤는데, 생각보다 쉬워서 좋았다.

    @import '../../../style/variable.scss'; // 변수 정리한 파일
    @import '../../../style/mixin'; // 화면크기 설정된 파일
    
    @include mobile{
    .chat-title{
      line-height: 50px;
      font-size: $font-medium;
    }
    .chat-container {
      width: 90%;
    }
    }
  2. 랜딩페이지 시작 버튼

    꼭 넣고 싶었던 버튼이었다.
    반질반질하게 빛나는 버튼!
    출처 를 참고했다.

  3. 일정금액 제한
    등록페이지에서 경매최소금액 21억미만의 제한을 두고,
    입찰버튼으로 21억초과해서 입찰할 수 없게 했다.
    데이터베이스에 들어가는 값에 한계가 있었기 때문이다.

  4. XSS 공격 방지
    물품 등록할 때, 제목과 설명란에 html태그가 들어가도 적용되지 않도록
    빈문자열로 변경되게 설정하였다.

 const newText = e.target.value.replace(/(<([^>]+)>)/ig, ''); // html 태그 제거
    setTitle(newText);
  1. 물품 등록 시 시간 재설정
    각자 로컬에서 테스트를 할 때는 시간에 문제가 없었는데,
    1차 배포를 진행하고, 물품등록을 테스트를 했다.
    등록화면에서는 문제가 없었는데 검색 페이지에서 설정한 시간보다 9시간씩 늘어나 있었다.
    아마 AWS 클라우드 프론트가 버지니아 쪽에 있어서 시간이 그 쪽에 맞춰지는 것 같다는 팀원의 의견이 일리가 있었다.
    그래서 클라이언트에서 서버에 보낼 때, 시간을 9시간 빼서 보내게 설정했다.
if (e.currentTarget.value === '1d') {
      date.setDate(date.getDate() + 1);
      setEndtime(getTime());
      date.setHours(date.getHours() - 9); // 추가한부분
      setFixTime(getTime()); // 추가한부분
    } else if (e.currentTarget.value === '3d') {
      date.setDate(date.getDate() + 3);
      setEndtime(getTime());
      date.setHours(date.getHours() - 9); // 추가한부분
      setFixTime(getTime()); // 추가한부분
    } else if (e.currentTarget.value === '7d') {
      date.setDate(date.getDate() + 7);
      setEndtime(getTime());
      date.setHours(date.getHours() - 9); // 추가한부분
      setFixTime(getTime()); // 추가한부분
    }
    changeSelectedPeriodBtnColor(e.target as HTMLElement);
  1. 도장찍힐 때 이펙트

입찰을 하면 최고가입찰이라는 것을 사용자에게 알려주기 위해 왕관도장이 생겼었다.
그런데 이펙트가 따로 없어서 밋밋한 느낌이 들어서
도장이 찍히는 듯한 이펙트를 넣어줬다.
일단 준비한 이펙트를 팀원들에게 보여줬는데 반응이 너무 좋아서, 바로 적용시켰다.
아래는 해당 이펙트 코드이다.

.itemcard-stamp {
  position: absolute;
  top: -18%;
  left: -5%;
  width: 30%;
  border-radius: 50px;
  animation-name:spinCircle;
  animation-duration:.8s;
  animation-iteration-count: 1;
}

@keyframes spinCircle {
  from {
      background-color: gray;
  }
  to {
      background-color: rgba(255, 255, 255, 0);
  }
}

⚝ 느낀점

  1. 회의 지옥이었다. (그런데 짜릿함을 곁들인)
    다른 팀의 경우 홈페이지 컨셉, API,디자인 등등 각자 역할을 나눠서 진행한 것 같다.
    하지만 우리팀은 모든 걸 함께했다.
    역할을 나눠서했다면 1시간만에 할 수 있는 걸 모여서 하니 4시간이 되었다.
    당연하게도 아침 10시에 시작한 회의는 새벽 2시가 넘도록 계속됐다.
    이런 방식이 싫었지만 싫지 않았다.

    1. 내가 좋은 의견을 냈을 때, 감탄하는 팀원들의 모습
    2. 내가 생각해내지 못한 의견을 내는 팀원에 환호하는 그 순간
    3. 좋은 아이디어들을 가려내기 위해 투표를 해서 최고의 아이디어가 선정되는 것

    이 세가지에 중독이 되었다. 😇
    아이디어, 사이트이름, 홈페이지 색상, 주요기능 심지어 폰트까지 모든 팀원의 의견이 들어가 있다. 그래서 모두가 만족할 수 있는 결과가 나왔다고 생각한다.
    주로 각자 의견을 하나씩 낸 뒤에 투표를 통해 결정했고, 다들 결과를 받아들였다.
    이런 점이 프로젝트가 완성도 높고, 모두가 즐겁게 회의를 할 수 있었던 기반이라고 생각한다.

  2. 테스트의 연속이다.
    기능 하나 만들 때마다 제대로 작동하는지, 예상치 못하게 다른 부분이 영향받지는 않는지 모든 화면을 다 검토하였다. 엉뚱한 화면들이 많아서 다들 웃으면서 수정했다.
    참고로 우리가 카카오 API를 호출한 횟수이다. (21.04.27 기준)

  3. 팀의 소중함을 알게 되었다.
    처음 개발공부를 시작할 때에는 혼자서 모든 걸 다 하겠다는 생각이었다.
    사이트를 디자인하고 기능을 만들고 배포를 하는 것까지!
    그런데 코드스테이츠에서 페어로 공부를 하고, 팀프로젝트를 진행하면서 생각이 바뀌었다.
    혼자 하는 개발보다 다른사람과 소통하는 개발이 더 즐거웠고, 든든했다.
    이렇게 프로젝트를 완성했지만 여기서 내가 만든건 1/4 에 불과하다.
    자신있는 부분은 내가 나서서 구현했고, 내가 부족한 부분은 팀원들이 채워줬다.
    우리 팀이 아니었다면 이렇게 멋진 결과를 만들 수 없었을 것이다.

  4. 끝으로 코드스테이츠 커리큘럼 너무 좋다.
    첫 페어와 막막하게 코플릿을 풀던 기억이 스쳐지나간다.
    힘겨운 개발세팅을 겪고, 각종 에러메세지를 파악하는 법을 배웠다.
    수강을 시작하기 전에 한 후기글을 보고 마음을 결정했는데 그 멘트가 아직도 생각난다.
    물고기를 잡아 주는 것이 아닌, 물고기를 잡는 법을 알려주는 곳이라고.
    내가 원하던 방식이라고 생각해서 등록하게 되었고, 실제로 나는 물고기를 잡을 수 있게 되었다.
    이렇게 코스를 잘 마무리하게 되어서 기쁘다.
    여태 배운 것을 기반으로 앞으로도 열심히 공부하는 개발자가 될 것이다.


서버 레포
클라이언트 레포

profile
기록하는 백엔드 개발자

0개의 댓글

관련 채용 정보