TIL | WIL

2/2 목 (다대다 관계 테이블은 이렇게 짜면 된다) TIL, TIT

깊은바다거북 2023. 2. 2. 22:29

(베이커리 이커머스 프로젝트 2일차)

오늘 한 일

  • 어제 만든 DB 스키마를 보강하고 sequelize 모델을 만들었다. 깃험에 pull 완료.

    ⇒ ‘product’와 ‘cart’ 테이블의 관계에서 Many-to-Many를 굳이 적용해야 하나를 고민하다가 (mapping 테이블이 하나 더 만들어진다는 게 꺼려졌다) 이게 사실은 One-toMany 관계라는 것을 깨달았다. 기존의 ‘cart’ 테이블 자체가 ‘user’와 ‘product’를 Many-to-Many로 연결지어주는 매핑 테이블이었던 셈이다. ‘cart’라는 테이블명 때문에 더 헷갈렸던 것 같아서, ‘cart-item’이라고 이름을 바꿔주었다.

    ⇒ 참고로 user와 product 테이블이 Many-to-Many인데 중간에 cart_item이 매핑 테이블 역할을 하고 있고,

    ⇒ order와 product 테이블도 Many-to-Many인데 중간에 order-item이 매핑 테이블 역할을 하고 있는 형국이다.

  • dotenv 모듈 도입, Sequelize의 사용자 비번 등을 환경 변수로 교체 완료. 깃헙에 pull 완료
  • ‘상품’ CRUD 반절

아래는 수정 전과 수정 후 ERD:

어제 작성한 ERD
오늘 보강한 스키마

로컬 ↔ 원격 pull/push 를 위한 명령어

origin이라는 이름의 원격(깃허브) 레파지토리의 ‘feature/mypage’ 브랜치에 내 로컬 ‘master’브랜치를 푸시하고 싶을 때:

git push origin master:feature/mypage

내 현재 브랜치로 원격 origin/develop 브랜치를 pull 해오기:

git pull origin develop

내일 할 일:

  • ‘상품’ CRUD 작성 완료하기.
  • 프론트 작성 시작 및 ejs 조사
  • (옵션)nodemon 환경 세팅은 프론트를 만들 때 하자.
    "scripts": {
      "start": "nodemon app.js"
    },
    nodemon.json 설정파일 없이 그냥 이걸로 충분한가? 
    // nodemon.json 예시
    {
      "watch": ["index.ts"],
      "ext": ".ts, .js",
      "ignore": [],
      "exec": "npx ts-node ./index.ts"
    }
  • (옵션)테스트에 Joi 활용을 고민중. 일단은 API 완성이 먼저다.


파이참에서 파일 확장자 인식 못함 오류

발생 상황:

파이참 에디터가 갑자기 자바스크립트 코드를 코드로 인식하지 않는 문제가 발생했다. 예를 들어 줄바꿈 시 자동 들여쓰기나 바디 블록 인식이 전혀 안 된다. 코드 자동완성 기능도 없어졌다.

시도:

파이참 IDE 전체 껐다 켜보기 ⇒ 안됨.

파이참 Settings에서 이리저리 건드려 보았으나 잘 안됨.

원인:

파이참이 자바스크립트 확장자를 가진 파일을 text 파일 타입으로 인식하고 있었음.

해결:

  1. Settings > Editor > File Types > “Text”를 찾아 등록된 패턴 “*.js” 삭제
  1. Settings > Editor > File Types > “JavaScript”를 찾아 “*.js”패턴 추가

특별히 건드린 게 없는데 갑자기 왜 이렇게 설정되어 있던 건진 모르겠으나, 이것으로 해결 완료됨.

(참고: https://ahn3330.tistory.com/63)

async/await과 promise.then/catch 사용 비교

  • async/await을 사용하면 await가 대기를 처리해주기 때문에 .then이 거의 필요하지 않다. (= async 함수도 promise를 반환하는 건 마찬가지이므로 .then을 붙이는 게 가능하다.)
  • 여기에 더하여 .catch 대신 일반 try..catch를 사용할 수 있다는 장점도 생긴다. (=.catch를 쓰는 것보다 try…catch 구문이 더 좋게 보인다는 거군.)

⇒ 결론: async/await를 사용하면 promise.then/catch가 거의 필요 없다.

async를 사용하면서도 promise.then/catch를 사용하는 경우 :

  1. async 함수 내에서 await으로 발생한 ‘프라미스 거부 상태’를 try…catch로 처리해주기를 깜빡할 경우를 대비하여.
  1. 가장 바깥 스코프에서 비동기 처리가 필요할 때.

    async 함수 바깥인 최상위 레벨 스코프에선 (당연한 얘기지만) await을 사용할 수 없으므로 보통의 비동기 함수를 사용하게 되는데, 이 경우 거부된 프라미스를 처리할 수 있는 유일한 방법은 비동기 함수 f().catch()를 추가하는 방법 뿐이다.

⇒ 결론: 1과 2의 경우 모두를 아울러서 관행처럼 (async 비동기 함수든 최상위 스코프에서 (어쩔 수 없이) 사용되는 일반 비동기 함수든) 끝에 .then/catch를 추가해 최종 결과나 처리되지 못한 에러를 다룬다.

// .catch 처리 예시: 
async function f() {
  let response = await fetch('http://유효하지-않은-주소');
	// try...catch로 처리하는 것을 잊음.
}

// f()는 거부 상태의 프라미스가 됩니다.
f().catch(alert); // TypeError: failed to fetch // (*)

(참고: https://ko.javascript.info/async-await#ref-1373)

  • (팁) 최상위 레벨에서 await 사용하기

    : async 익명 즉시 호출 함수로 코드를 감싸면 된다.

    (async () => {
      let response = await fetch('/article/promise-chaining/user.json');
      let user = await response.json();
      ...
    })();

적용 - 에러 발생:

3계층 애플리케이션을 만들고 있는데, service나 repository 계층에서

  1. await으로 작성한 코드에서 프라미스 거부가 발생하거나,
    // services/product.service.js
    ...
    updateProduct = async (product_id, ...) => {
      // product_id로 수정할 데이터가 있는지 검사.
      const findProduct = await this.productRepository.findProductById(product_id);
      if (!findProduct) { 
        throw new Error("Product doesn't exist");
      }
    	...
    }
  1. 직접 에러를 만들어 던질 수 있다.
    // services/product.service.js
    ...
    updateProduct = async (product_id, ...) => {
      // product_id로 수정할 데이터가 있는지 검사.
      const findProduct = await this.productRepository.findProductById(product_id);
      if (!findProduct) { 
        throw new Error("Product doesn't exist");
      }
    	...
    }

어느 쪽이든 상위 계층인 controller에서 try…catch 문으로 받으면 된다!

적용 - 에러 처리:

catch(error)에 들어오는 error가 어느 계층에서 발생된 에러든(즉 몇 단계를 거쳐오든), 직접 던진 에러든 await이 발생시킨 에러든 훌륭히 다 받는다.

// controllers/product.controller.js
updateProduct = async (req, res, next) => {
	const { product_id } = req.params;
  ...

  try {
    const updated = await this.productService.updateProduct(product_id, ...);
    return res.status(200).json({
      message: '상품 수정이 완료되었습니다.',
    });
  } catch (error) {
    console.log(error.message);
    return res.status(500).json({
      errorMessage: error.message,
    });
  }
};
  1. 이 전 계층인 product.service.js에서 1번 상황, 즉 await 부분에서 프라미스 거부가 발생한다면 프라미스가 발생시킨 에러 메세지가 errorMessage에 담기게 된다. 더 전 계층인 repository.js에서 에러가 발생한 경우가 바로 이 경우이겠다.
  1. (await 부분을 잘 통과하고서) 직접 만든 에러가 던져진 경우, 여기서 작성한 메세지 “Product doesn’t exist”가 errorMessage에 담기게 된다. 이 전 repository 계층에서 findByPk 쿼리로 찾은 결과가 null일 때(=아무것도 못찾은 경우)가 이 경우에 해당하게 된다.

괜히 코드를 지저분하게 쓰는 것도 아니고 다른 더 나은 대안이 있는데 어쩌다 보니 이런 식으로 쓰는 것도 아니라, 바로 이 방법이 정석이다. 라고 나름 결론을 내릴 수 있어 속이 시원하다.

Sequelize에 dotenv 적용시키기

Sequelize를 세팅하면서 만든 원래의 파일들에서 세 가지만 바꿔주면 된다.

  1. config/config.json 파일을 config/config.js 파일로 바꿔주기.
    // config.json
    {
      "development": {
        "username": "root",
        "password": "12341234",
        "database": "db_development",
        "host": "some.database.rds.amazonaws.com",
        "dialect": "mysql"
      },
      "test": { ... },
      "production": { ... }
    }
    // config.js
    require('dotenv').config();
    
    const development = {
      username: process.env.MYSQL_AWS_USERNAME,
      password: process.env.MYSQL_AWS_PASSWORD,
      database: process.env.MYSQL_AWS_DATABASE,
      host: process.env.MYSQL_AWS_HOST,
      dialect: 'mysql',
    };
    const test = { ... };
    const production = { ... };
    
    module.exports = { development, test, production };
  1. model/index.js > config 부분을 수정하기
    // model/index.js
    ...
    const config = require(__dirname + '/../config/config.json')[env];
    const config = require(__dirname + '/../config/config.js')[env];
  1. package.json파일과 같은 레벨에 .env 파일을 하나 만들고 필요한 환경 변수를 몽땅 적어 넣는다.
    // .env
    PORT=7000
    ...
    MYSQL_AWS_USERNAME=root
    MYSQL_AWS_PASSWORD=12341234
    MYSQL_AWS_DATABASE=db_development
    MYSQL_AWS_HOST=some.database.rds.amazonaws.com
  1. .gitignore 파일에 .env를 추가한다.

(참고: https://velog.io/@hyunju-song/sequelize로-DB셋팅할-때-환경변수-파일-설정-및-사용하기)

고민중…

DB에서 가져올 때부터 컬럼을 고르거나 정렬해서 가져오는 게 더 나을까?

  • findAll 전체 가져와서 필요한 컬럼 고르고 데이터 trim하기 vs. 처음부터 몇 컬럼만 가져오기
  • findAll 전체 가져와서 정렬하기 vs. 처음부터 order by로 가져오기

Sequelize findAll의 limit과 offset 옵션으로 진짜 페이지네이션이 가능할까?

여기 (빈약한) 참조: https://sequelize.org/docs/v6/core-concepts/model-querying-basics/#limits-and-pagination

Limits and Pagination

The limit and offset options allow you to work with limiting / pagination:

// Fetch 10 instances/rows
Project.findAll({ limit: 10 });

// Skip 8 instances/rows
Project.findAll({ offset: 8 });

// Skip 5 instances and fetch the 5 after that
Project.findAll({ offset: 5, limit: 5 });

Usually these are used alongside the order option.

Joi로 검사하기

튜터님 코드 뜯어보는 중…

찾은 코드 조각 및 용례:

// routes/comments.js
const {
  commentCreateValidation,
  commnetUpdateValidation,
} = require('../validations');
// routes/comments.js
...
} catch (err) {
    if (err.isJoi) {
      return res.status(422).json({ message: err.details[0].message });
    }
    res.status(500).json({ message: err.message });
}
// validations/index.js
const Joi = require('joi');

const signupValidation = Joi.object({
  nickname: Joi.string().alphanum().not('').required(),
  password: Joi.string().min(3).not('').required(),
  confirm: Joi.equal(Joi.ref('password')).required().messages({
    'any.only': '비밀번호가 일치하지 않습니다.',
  }),
});
const postCreateValidation = Joi.object({ ... })

// ...

module.exports = {
  signupValidation,
  postCreateValidation,
  ...
};


Uploaded by N2T