3. 개발노트 3주차

daitnu·2019년 11월 23일
1

들어가며

저희는 메일 서비스를 만들기 위해 RDBMS를 사용합니다.
현재 4차 수정까지 진행됐으며, 아래와 같습니다.
왜 이런 ERD가 나왔는지는 각자 구현 내용을 바탕으로 설명될 것입니다.

Database 참고 코드

현재까지 구현된 VIEW

로그인

회원가입

메일 리스트

메일쓰기

메일읽기

배형진

  • 2주차 코드리뷰

  • 피드백에 의한 리팩토링
  const insertQuery = 'INSERT INTO ?? SET ?';
  const tableName = 'tbl_mail_template';
  const now = mysql.raw('now()');
  const mailTemplate = { from, to, subject, text, created_at: now, updated_at: now };
  const queryFormat = mysql.format(insertQuery, [tableName, mailTemplate]);
  • 느낀점

이전까지는 valueescaping 처리를 수행하였는데 indentifier까지 escaping 처리가 가능하다는 것을 깨달았습니다. 이번 기회에 SQL 문을 직접 문자열로 만들어서 실행시켰을 때 SQL Injection에 취약하다는 것을 인지하게 되었고 덕분에 SQL Injection에 대해 조사하게 되었습니다. 개발자라면 최소한의 보안 지식도 필요하다는 것을 다시 한번 느끼게 되었습니다.

참고

성재호

  • 이번주에는 메일 전송에 관련한 FE/BE를 구현하였습니다
  • 메일 전송을 구현하며 있었던 이슈와 코드 리뷰를 받으며 개선해 나아간 부분에 대하여 말씀드리고자 합니다.

첫 번째로 메일 전송을 구현하며 있었던 이슈입니다.

  • 우선 메일 전송 과정은 이러합니다
  • 메일 템플릿 생성 -> 첨부파일 생성 -> 메일 레코드 생성 -> 메일 전송
  • 저희 팀은 이 전체의 과정을 트랜잭션을 걸어서 메일 저장에 실패하면 데이터베이스에 들어가지 않도록 하자는 의견과 트랜잭션의 비용이 비싸기 때문에 메일 전송에 실패하고 남은 쓰레기 값들은 나중에 crontab을 돌려 제거하자는 의견으로 나뉘었었습니다.
  • 현재는 트랜잭션을 사용하고 있지만 이것이 최선인가 하는 의문이 들어 현업 개발자분께 여쭈어보았습니다. 해주신 답변은 매우 만족스러웠고 다음 주에 개발자분께서 말씀해주신 내용을 바탕으로 다시 한번 토론을 해보아야 하겠지만 현재의 트랜잭션을 유지하는 방향으로 갈 것 같습니다.

두번째로 리액트에서의 재렌더링에 관련한 이슈였습니다.

  • 우선 재렌더링에 관련하여 두 가지 이슈가 있었는데요.
  • 하나는 메일 전송 컴포넌트에서 context api에 관련한 이슈였습니다.
  • 메일 전송에 관련하여 프론트 작업을 하였고 컴포넌트를 잘게 나누어보고자 하였었습니다.
  • 이렇게 개발을 하려다 보니 context api를 사용하는 방향으로 개발을 진행하였고 하나의 context 안에 모든 데이터를 넣고 모든 컴포넌트들에게 데이터를 주게끔 코드를 작성하였었습니다.
  • 이러다 보니 context내의 데이터 하나만 바뀌더라도 모든 컴포넌트들이 재렌더링되는 현상이 나타나 성능에 있어서 매우 나쁜 코드를 작성한 셈이었습니다.
  • 이를 개선하기 위해 useMemo, React.memo와 같이 memoization을 사용하는 방법과 useReducer를 사용하여 context의 구조를 바꾸는 방법이 있었지만 현업 개발자분께서 context의 구조를 바꾸어보는 방향을 권해주셨기 때문에 context의 구조를 바꾸는 방법을 선택하여 context하나는 state만 내려주고 하나는 dispatch만 내려주어 재렌더링을 방지하도록 리팩토링을 진행하였습니다.
import React, { useReducer, useContext } from 'react';
import { writeMailReducer } from './reducer';
import { initialState } from './reducer/initial-state';

const WMstateContext = React.createContext();
const WMdispatchContext = React.createContext();

export const WriteMailContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(writeMailReducer, initialState);
  return (
    <WMstateContext.Provider value={state}>
      <WMdispatchContext.Provider value={dispatch}>{children}</WMdispatchContext.Provider>
    </WMstateContext.Provider>
  );
};

export const useWriteMailState = () => {
  const ctx = useContext(WMstateContext);
  if (ctx === undefined) {
    throw new Error('useWriteMailState은 WriteMailContextProvider 내에서 사용할 수 있습니다');
  }
  return ctx;
};

export const useWriteMailDispatch = () => {
  const ctx = useContext(WMdispatchContext);
  if (ctx === undefined) {
    throw new Error('useWriteMailDispatch은 WriteMailContextProvider 내에서 사용할 수 있습니다');
  }
  return ctx;
};
  • 두 번째 재렌더링 이슈는 제목과 내용 입력에 관련된 부분이었습니다.
  • 컴포넌트의 value속성에 state값을 넣어주고 onChange때마다 setState를 해주다 보니 제목이나 내용을 입력할 때마다 재렌더링이 되는 이슈가 있었습니다.
  • 이를 개선하고자 onBlur만 사용하면 되겠다는 생각을 하였지만 제목과 내용의 state값들은 모두 context내에서 관리하고 있었습니다.
  • 나중에 submit버튼을 누르면 제목과 내용 그리고 첨부파일까지 모두 초기화하는 방향으로 구현해나가고 있었기 때문에 제목과 내용에는 value속성을 state로 줄 수밖에 없는 상황이었습니다.
  • 하지만 value 속성을 state로 주려면 onChange속성 또한 줘야 하는 불가피한 상황이었습니다.
  • 이를 해결하고자 flag라는 state를 두어 focus했을 때는 value속성을 주지 않고 제목이나 내용 statedefaultValue를 주고 focusout(blur)시에는 value속성을 주는 방법으로 개선하여 submit button을 누르면 focusout상태이기 때문에 제목이나 내용의 value값이 context로부터 받아온 state이므로 변경이 가능했고, onChange를 사용하지 않고 onBlur 이벤트가 발생했을 때에만 contextstate값을 바꾸는 dispatch함수를 실행하기 때문에 제목이나 내용을 입력할 때마다 재렌더링 되는 것을 방지할 수 있었습니다.
import React, { useState } from 'react';
import * as S from './styled';
import * as WM_S from '../styled';
import { useWriteMailState, useWriteMailDispatch } from '../ContextProvider';
import { UPDATE_SUBJECT } from '../ContextProvider/reducer/action-type';

const InputSubject = () => {
  const { subject } = useWriteMailState();
  const dispatch = useWriteMailDispatch();

  const [flag, setFlag] = useState(true);

  const blurHandler = e => {
    dispatch({ type: UPDATE_SUBJECT, payload: { subject: e.target.value } });
    setFlag(!flag);
  };

  const focusHandler = _ => {
    setFlag(!flag);
  };

  return (
    <WM_S.RowWrapper>
      <WM_S.Label>제목</WM_S.Label>
      {flag ? (
        <S.InputSubject maxLength={50} onFocus={focusHandler} value={subject} />
      ) : (
        <S.InputSubject maxLength={50} onBlur={blurHandler} defaultValue={subject} />
      )}
    </WM_S.RowWrapper>
  );
};

export default InputSubject;

이정환

웹 페이지에서 회원가입을 하여 웹 메일 서비스를 이용할 수 있도록 해보았습니다.

Server

먼저 회원 테이블 구조입니다.

// server/src/database/models/user.js

...

const User = sequelize.define(
'User',
{
  no: {
    type: DataTypes.BIGINT.UNSIGNED,
    primaryKey: true,
    autoIncrement: true,
  },
  id: {
    type: DataTypes.STRING(255),
    allowNull: true,
    unique: true,
  },
  domain_no: {
    type: DataTypes.BIGINT.UNSIGNED,
    allowNull: false,
    defaultValue: 1,
  },
  name: {
    type: DataTypes.STRING(255),
    allowNull: false,
  },
  password: {
    type: DataTypes.STRING(255),
    allowNull: false,
  },
  email: {
    type: DataTypes.STRING(255),
    allowNull: true,
    unique: true,
  },
  sub_email: {
    type: DataTypes.STRING(255),
    allowNull: false,
    unique: true,
  },
  salt: { 
    type: DataTypes.STRING,
    allowNull: true,
  },
},
);

...

지난주 dovecot-sql.conf.ext 설정에서 암호화 방식을 SHA512-CRYPT으로 설정했습니다.

또한 MySQL을 통해 다음과 방식으로 유저를 추가한 적이 있었습니다.

INSERT INTO `mailserver`.`virtual_users`
   (`id`, `domain_id`, `password` , `email`)
 VALUES
   ('1', '1', ENCRYPT('password', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'email1@example.com'),
   ('2', '1', ENCRYPT('password', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'email2@example.com');
  • ENCRYPT("password", CONCAT('$6$',sha(RAND()))) 해당 부분이 MySQL에서 SHA512-CRYPT scheme 방식을 사용하여 비밀번호를 암호화하는 방법입니다.
  • 이때 RAND()는 0에서 1 사이의 랜덤 값을 생성하며 sha(str)는 str로부터 160비트의 해시값을 만들어 냅니다.
  • 랜덤으로 생성한 값과 함께 password를 암호화를 하기 때문에 랜덤으로 생성한 값을 회원마다 저장할 필요가 있습니다.
  • 따라서 회원에서 salt 필드를 두어 랜덤으로 생성한 값을 따로 저장하여 로그인 시 salt 값을 사용하여 password를 인증할 수 있습니다!

회원가입 API

  1. mysql을 이용한 salt 생성, 암호화 코드 작성
// server/src/libraries/crypto.js
import DB from '../database';

// MySQL의 SUBSTRING, SHA, RAND 함수를 사용하여 salt를 생성합니다.
export const createSalt = async () => {
  const [saltQueryresult] = await DB.sequelize.query('SELECT SUBSTRING(SHA(RAND()), -16)', {
    raw: true,
    type: DB.sequelize.QueryTypes.SELECT,
  });

  const [saltKey] = Object.keys(saltQueryresult);
  const salt = saltQueryresult[saltKey].toString();

  return salt;
};

// MySQL의 ENCRYPT 함수를 통해 password, salt를 받아 암호화합니다.
export const encrypt = async (password, salt) => {
  const [passQueryResult] = await DB.sequelize.query(
    `SELECT ENCRYPT('${password}', CONCAT('$6$', '${salt}'))`,
    {
      raw: true,
      type: DB.sequelize.QueryTypes.SELECT,
    },
  );

  const [passKey] = Object.keys(passQueryResult);
  const hashedPassword = passQueryResult[passKey].toString();

  return hashedPassword;
};
  1. sequelize hooks
// server/src/database/models/user.js

...
const convertToUserModel = async instance => {
  const { id, password } = instance.dataValues;

  const salt = await createSalt();
  
  const hashedPassword = await encrypt(password, salt);

  instance.email = `${id}@${DEFAULT_DOMAIN_NAME}`;
  instance.password = hashedPassword;
  instance.salt = salt;
};

...

// instance의 값을 수정하여 DB에 저장할 수 있습니다.
User.beforeCreate(async instance => {
  await convertToUserModel(instance);
});

...
  • beforeCreate는 만들어진 instance가 만들어지기 전에 호출되는 함수로 instance 값을 수정시 저장되는 값에 반영이됩니다.
  • convertToUserModel에서는 instance의 password를 암호화합니다.
  • id 정보로 부터 만든 email, 암호화에 사용된 salt도 함께 instance에 저장합니다.
  1. 서비스 계층 코드 작성
// server/src/v1/users/service.js
...
const register = async ({ id, password, name, sub_email }) => {
  const userData = { id, password, name, sub_email };
  let newUser;

  try {
      const [response, created] = await DB.User.findOrCreateById(userData, { transaction });
      if (!created) {
        throw new ErrorResponse(ERROR_CODE.ID_DUPLICATION);
      }
      newUser = response.get({ plain: true });
  } catch (error) {
    throw error;
  }

  return newUser;
};
...
  • 사용자로 부터 받은 회원 데이터를 DB에 넣어줍니다.
  • 이미 등록된 아이디, 이메일이 있을 경우 에러를 throw 합니다.
  • 생성된 회원의 password, salt 필드를 삭제한 후 반환합니다.
  1. controller 코드 작성
const registerUser = async (req, res, next) => {
  let newUser;

  try {
    await validation.join(req.body);
    
    newUser = await service.register(req.body);
    delete newUser.password;
    delete newUser.salt;
  } catch (error) {
    return next(error);
  }

  return res.status(status.CREATED).json({ newUser });
};
  • validation.join을 통해 req.body가 유효한 값인지 판단합니다. 유효하지 않을 경우 에러를 throw 합니다.
  • service.register를 통해 DB에 회원을 등록합니다.
  • catch 블럭안의 next를 통해 발생한 에러를 따로 처리하도록 하였습니다.

로그인 API

  1. 서비스 계층 코드 작성
...

const localLogin = async ({ id, password }) => {
  const user = await DB.User.findOneById(id);

  if (!user) {
    throw new ErrorResponse(ERROR_CODE.INVALID_LOGIN_ID_OR_PASSWORD);
  }

  const hashedPassword = await encrypt(password, user.salt);

  const match = user.password === hashedPassword;

  if (!match) {
    throw new ErrorResponse(ERROR_CODE.INVALID_LOGIN_ID_OR_PASSWORD);
  }

  return user;
};
...
  • 입력한 password는 회원가입을 할 때 생성한 user의 salt와 함께 암호화하여 user의 password와 비교합니다.
  • 같을 경우 user를 반환하고 다를 경우 에러를 throw 합니다.
  1. passport LocalStrategy 작성
passport.use(
  new LocalStrategy(
    {
      usernameField: 'id',
      passwordField: 'password',
    },
    async (id, password, done) => {
      let user;
      try {
        user = await authService.localLogin({ id, password });
        delete user.password;
        delete user.salt;
      } catch (err) {
        return done(err);
      }
      return done(null, user);
    },
  ),
);
  • DB와 관련된 로직은 분리하여 코드가 간결해졌습니다.
  • user의 password, salt는 삭제하도록 하였습니다.
  1. passport 미들웨어 사용
router.post('/login', validateLogin, passport.authenticate('local'), ctrl.login);

홍종화

이번 주에 페이징을 담당해서 구현했습니다.

메일 리스트 하단부에

이러한 버튼이 있는데요. 버튼을 눌렀을 때 해당 mail data를 가져와 화면에 뿌려줍니다.
또한 이런 버튼들을 만들어주기 위해서 paging 정보가 있어야 했기 때문에 이와 관련된 BE 작업을 진행하였습니다.

페이징 처리를 하기 위해서는 한 페이지에 보여주는 perPageNum이라는 변수가 필요합니다.
또 한 번에 페이지 버튼을 몇 개 보여줄지 정해야 했기 때문에 pageListNum이라는 변수가 필요했습니다.

페이징 처리를 할 때 생각해봐야 할 것이 몇 가지 있었습니다.

  1. 첫 번째 index라면 <-(왼쪽 화살표)가 없어야 합니다.
  2. 마지막 index라면 ->(오른쪽 화살표)가 없어야 합니다.
  3. 첫 번째 마지막 둘 다 아니라면 < > 가 모두 존재해야 합니다.
  4. perPageNum에 따라 totalPage가 변합니다.

server

const DEFAULT_PAGE_LIST_NUM = 10;
const DEFAULT_PER_PAGE_NUM = 100;
const DEFAULT_PAGE_NUM = 1;

/**
 * @param {Number} totalCount 메일함에있는 메일
 * @param {Object} options
 * @param {Number} options.page 현재 페이지
 * @param {Number} options.perPageNum 한페이지당 출력할 갯수
 * @returns {Object} paging result
 */

const paging = (totalCount, options = {}) => {
  const perPageNum = options.perPageNum || DEFAULT_PER_PAGE_NUM;
  let page = options.page || DEFAULT_PAGE_NUM;

  const totalPage = Math.ceil(totalCount / perPageNum) || 1;

  if (totalPage < page) {
    page = totalPage;
  }

  const startPage = Math.floor((page - 1) / DEFAULT_PAGE_LIST_NUM) * DEFAULT_PAGE_LIST_NUM + 1;
  let endPage = startPage + DEFAULT_PAGE_LIST_NUM - 1;
  endPage = endPage > totalPage ? totalPage : endPage;
  return { startPage, endPage, page, perPageNum, totalPage };
}

export default paging;

위 코드는 위 조건들을 만족하는 코드인데요.
현재의 pagetotalPage보단 클 수 없기 때문에 if를 통하여 값을 재설정해줍니다.
또한 endPagetotalPage보다 클 수 없기 때문에 if를 통하여 값을 재설정 해줍니다.
해당 정보를 client로 보내고 이를 client에서 처리해야 합니다.

front

const getPageNumberRange = index => {
  const start = index * PAGE_LIST_NUM;
  return [start + 1, start + PAGE_LIST_NUM];
};

const Paging = ({ paging }) => {
  const { page, startPage, totalPage } = paging;
  const currentIndex = Math.floor(startPage / PAGE_LIST_NUM);
  const lastIndex = Math.floor(totalPage / PAGE_LIST_NUM);
  const [index, setIndex] = useState(currentIndex);
  const { dispatch } = useContext(AppContext);

  const classes = useStyles();

  const pagingNumber = [];
  const [startNumber, endNumber] = getPageNumberRange(index);
  for (let i = startNumber; i <= endNumber && i <= totalPage; i += 1) {
    const number = <PageNumber key={i} id={i} color="secondary" onActive={page === i} />;
    pagingNumber.push(number);
  }

  const handleMoveBtnClick = value => {
    const newIndex = index + value;
    const [newPageNumber] = getPageNumberRange(newIndex);
    setIndex(newIndex);
    dispatch(handlePageNumberClick(newPageNumber));
  };

  const handleNumberClick = (e) => {
    e.preventDefault();

    const { id } = e.target;
    if (!id || id === '') {
      return;
    }

    if (Number.isInteger(id)) {
      return;
    }

    dispatch(handlePageNumberClick(Number(id)));
  };

  return (
    <GS.FlexRowWrap onClick={handleNumberClick}>
      <Fab
        size="small"
        color="secondary"
        aria-label="add"
        className={classes.margin}
        onClick={() => handleMoveBtnClick(-1)}
        style={{ width: '35px', height: '10px ', display: index === 0 ? 'none' : '' }}>
        <ArrowBackIcon />
      </Fab>

      {[...pagingNumber]}

      <Fab
        size="small"
        color="secondary"
        aria-label="add"
        className={classes.margin}
        onClick={() => handleMoveBtnClick(1)}
        style={{ width: '35px', height: '10px ', display: index === lastIndex ? 'none' : '' }}>
        <ArrowForwardIcon />
      </Fab>
    </GS.FlexRowWrap>
  );
};

startPagetotalPage를 PAGE_LIST_NUM로 나눠 current indexlastIndex를 얻어냅니다.

이후 index를 아래 함수에 넣어 number의 값을 구합니다.

const getPageNumberRange = index => {
  const start = index * 10;
  return [start + 1, start + 10];
};

이 값을 가지고 for 문을 통해 페이징 숫자들을 만들어 냅니다. for 문을 사용해도 되지만, 가독성 면에서 떨어진다는 지적을 받았습니다. 블로깅 하는 시점에는 아직 반영이 안됐습니다.
버튼을 눌렀을 때, dispatch를 통해서 state를 바꿔야 하는데요. 숫자 하나하나에 이벤트를 달기보다 버블링을 이용한 델리게이션을 통해 구현했습니다.

  const handleNumberClick = (e) => {
    e.preventDefault();

    const { id } = e.target;
    if (!id || id === '') {
      return;
    }

    if (Number.isInteger(id)) {
      return;
    }

    dispatch(handlePageNumberClick(Number(id)));
  };

때문에 target.id를 통해서 id를 얻어와야 합니다. 이때 버튼이 아닌 부분이 클릭된 것이 아닌지 확인하는 작업이 필요하기 때문에 if 문을 거친 후 통해한 경우에만 dispatch가 발동됩니다.

handlePageNumberClick 함수의 경우 아래와 같습니다. page의 상태를 변경시킵니다.

export const handlePageNumberClick = page => {
  return {
    type: PAGE_NUMBER_CLICK,
    payload: {
      page,
    },
  };
};

page가 변경될 경우 상위 컴포넌트인 MailArea에서 데이터를 가져오도록 했습니다.
( 아직 구현에 집중하고 있기 때문에 useFetch를 추상화하지는 못했습니다. 또한 에러 처리도 다음 주에 진행할 예정입니다. )

const useFetch = ({ category, page }, callback) => {
  const [isLoading, setIsLoading] = useState(true);

  const fetchInitData = async URL => {
    setIsLoading(true);
    const { data } = await axios.get(URL);
    callback(data);
    setIsLoading(false);
  };

  useEffect(() => {
    const URL = `/mail?category=${category}&page=${page}`;
    fetchInitData(URL);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [page, category]);

  return isLoading;
};


const MailArea = () => {
  const { state, dispatch } = useContext(AppContext);
  const { category, page } = state;
  const callback = data => dispatch(handleMailsChange({ category, ...data.mails, page }));
  const isLoading = useFetch(state, callback);

  if (isLoading) {
    return <Loading />;
  }
 ...
}

다음으론 server에서 api 요청을 받았을 때 처리를 알아보겠습니다.

GET /mail 엔드 포인트는 페이징뿐만 아니라 메일함(category)이 변경될 때도 동작해야 했습니다.

server

컨트롤러

const list = async (req, res, next) => {
  const userNo = req.user.no;
  const { query } = req;

  let mails;
  try {
    checkQuery(query);
    mails = await service.getMailsByOptions(userNo, query);
  } catch (error) {
    return next(error);
  }

  return res.json({ mails });
};

servercontroller, service, ORM(DAO) 영역으로 레이어 계층을 형성하고 있습니다.
컨트롤러 계층에서는 입력값을 validation 하는 역할을 담당하도록 했습니다.
categorypage 같은 경우 default 값이 존재하기 때문에 입력으로 들어오지 않아도 되기 때문에, 입력이 들어왔을 경우에만 validation을 하게 됩니다.

import { isInt } from 'validator';
import ERROR_CODE from '../exception/error-code';
import ErrorField from '../exception/error-field';
import ErrorResponse from '../exception/error-response';

const { MAX_SAFE_INTEGER } = Number;
const PAGE_NUMBER_RANGE = { min: 1, max: MAX_SAFE_INTEGER };
const CATEGORY_NUMBER_RANGE = { min: 0, max: MAX_SAFE_INTEGER };

const checkQuery = ({ category, page }) => {
  const errorFields = [];

  if (category && !isInt(category, CATEGORY_NUMBER_RANGE)) {
    const errorField = new ErrorField('category', category, '유효하지 않은 값입니다.');
    errorFields.push(errorField);
  }

  if (page && !isInt(page, PAGE_NUMBER_RANGE)) {
    const errorField = new ErrorField('page', page, '유효하지 않은 값입니다.');
    errorFields.push(errorField);
  }

  if (errorFields.length > 0) {
    throw new ErrorResponse(ERROR_CODE.INVALID_INPUT_VALUE, errorFields);
  }

  return true;
};

export default checkQuery;

service


const DEFAULT_MAIL_QUERY_OPTIONS = {
  category: 0,
  page: 1,
  perPageNum: 100,
};

const getMailsByOptions = async (userNo, options = {}) => {
  const queryOptions = { ...DEFAULT_MAIL_QUERY_OPTIONS, ...options };
  let { category, page, perPageNum } = queryOptions;
  category = Number(category);
  page = Number(page);
  perPageNum = Number(perPageNum);

  const query = getQueryByOptions({ userNo, category, perPageNum, page });
  const { count: totalCount, rows: mails } = await DB.Mail.findAndCountAllFilteredMail(query);

  const pagingOptions = {
    page,
    perPageNum,
  };
  const pagingResult = getPaging(totalCount, pagingOptions);

  return {
    paging: pagingResult,
    mails,
  };
};

service 계층에서는 유효한 값이 들어왔다는 가정하에 로직이 작성됩니다. (컨트롤러에서 벨리데이션을 했기 때문)
하나의 엔드 포인트에서 category, page를 담당해야 하므로 ORM 쿼리 조차 복잡해지는데요 (추후에 sort search까지 처리해야 합니다)
이를 위해 조금 추상적인 쿼리를 만들게 됐습니다.

  Mail.findAndCountAllFilteredMail = ({
    userNo,
    mailFilter = {},
    mailTemplateFilter = {},
    options = {},
    paging = DEFALT_PAGING,
  }) => {
    return Mail.findAndCountAll({
      distinct: true,
      ...paging,
      where: {
        owner: userNo,
        ...mailFilter,
      },
      include: [
        {
          model: sequelize.models.MailTemplate,
          where: {
            ...mailTemplateFilter,
          },
          include: [
            {
              model: sequelize.models.Attachment,
            },
          ],
        },
      ],
      raw: true,
      ...options,
    });
  };

요청에 따른 유동적인 쿼리를 위해 where 절을 외부에서 입력받습니다.
때문에 service 계층에서는 쿼리를 만들 책임이 부여됩니다.
이를 위해서 service 계층에서 아래의 함수를 사용합니다.

const getQueryByOptions = ({ userNo, category, perPageNum, page }) => {
  const query = {
    userNo,
    options: {
      raw: false,
    },
    paging: {
      limit: perPageNum,
      offset: (page - 1) * perPageNum,
    },
    mailFilter: {},
  };

  if (category > 0) {
    query.mailFilter.category_no = category;
  }

  return query;
};

where 절에 default로 들어갈 때, 값이 들어가면 안 되는 경우가 있습니다.
이러한 경우 분기를 통해 해결합니다.

profile
부스트캠프 4기 멤버십 6조 개발노트

0개의 댓글