깊은바다거북
개발 공부 기록
깊은바다거북
전체 방문자
오늘
어제
  • 분류 전체보기 (219)
    • JAVA (9)
    • JavaScript (15)
    • 스파르타코딩클럽 (11)
      • [내일배움단] 웹개발 종합반 개발일지 (5)
      • [내일배움캠프] 프로젝트와 트러블 슈팅 (6)
    • SQL | NoSQL (4)
    • CS 등등 (0)
    • TIL | WIL (173)
    • 기타 에러 해결 (3)
    • 내 살 길 궁리 (4)

인기 글

최근 글

최근 댓글

태그

  • Preorder Traversal(전위 순회)
  • POST / GET 요청
  • 01. 미니 프로젝트
  • 트러블 슈팅 Troubleshooting
  • Linked List
  • Trie
  • Til
  • 시간 복잡도
  • BFS(너비우선탐색)
  • 프로그래머스
  • 자료 구조
  • Backtracking(백트래킹)
  • 최소 힙(Min Heap)
  • leetcode-cli
  • 혼자 공부하는 자바스크립트
  • DFS(깊이우선탐색)
  • TypeScript
  • Leetcode
  • 자잘한 에러 해결
  • Binary Tree(이진 트리)
  • 팀 프로젝트
  • 자바스크립트 기초 문법
  • TIT (Today I Troubleshot)
  • 코딩테스트 연습문제
  • 재귀 함수
  • BST(이진 탐색 트리)
  • tree
  • 최대 힙(Max Heap)
  • Inorder Traversal(중위 순회)
  • 점화식(Recurrence Relation)
hELLO · Designed By 정상우.
깊은바다거북

개발 공부 기록

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

    'TIL | WIL' 카테고리의 다른 글
    • 2/4 토 (ejs와 `express-ejs-layouts` 동시 사용시 가능한 url 경로는 오직 query string 방법뿐) TIL, TIT
    • 2/3 금 (merge 충돌 해결, 충돌 해결) TIL
    • 2/1 수 (4번째 프로젝트 시작) TIL
    • 1/31 화 (타입스크립트 엔티티 모음과 분류) TIL, TIT
    깊은바다거북
    깊은바다거북

    티스토리툴바