항해 3기 5주차 WIL 2021.10.11~2021.10.17

CH_Hwang·2021년 10월 17일
0

항해99

목록 보기
7/14

2021.10.11

정겨웠던 나의 개인프로젝트

새로운 프로젝트 날의 시작이다. 새로운 프로젝트는 자신의 템포를 찾기위해 도와줄 사이트로 히말라야 등반 도우미의 호칭인 sherpa라고 지었다.

오늘은 api설계, db설계, 그리고 초기 설정들 (폴더구조, 모듈설치, 프리티어 및 eslint 등)을 했다.

항해 미니프로젝트2(셰르파)🏔

개요

다른 사람과의 비교는 필요 없다! 오로지 과거의 나와 비교해서 내가 얼마나 성장했는지, 목표치를 잘 유지하는지를 산 모양의 그래프로 볼 수 있는 항해라는 산의 등산 도우미, 셰르파를 소개합니다.

팀원

  • Front-end(김동우, 안정우)
  • Back-end(황창환, 박재현)

기간

  • 20211011 ~ 20211016(6일간)

공통

  1. 로그인 회원가입
  2. 데이터베이스 그래프화

프론트엔드

그래프 라이브러리 d3사용
김동우: 회원가입 페이지, 메인페이지 그래프, Text, Input
안정우: 로그인 페이지, 메인페이지 작성 파트, Grid, Btn

백엔드

실시간 통신 웹소켓 지양
database: mysql (sequelize)

와이어 프레임




API


DB diagram

위에가 오늘 짠 설계에 대한 내용들이며 이를 바탕으로 확장해나갈 것이다.

db migrate 과정에서 계속 없는 컬럼을 찾길래 기존에 했던 migrate가 아닌 방법, models에 있는걸 sync해서 가져오는 방법을 사용하려고 바꾸는 중이다.결국 nodejs 교과서에 나와있는 방법, 즉 내가 개인프로젝트때 사용했던 syquelize sync를 이용하는 방법을 사용해서 mysql을 사용하는 방법을 택했다. 그리고 서버에 배포한 후 postman으로 잘 되나 확인했고, 잘 작동하는 것을 확인했다.

2021.10.12

생각보다 프론트와 백엔드 간의 진도라고 해야되나, 그런부분이 마음에 너무 많이 걸렸다. 백엔드는 거의 마무리 단계인데 프론트엔드는 아직 로그인, 회원가입 부분을 하고 메인페이지는 아직 시작도 안한지라 마음이 급해지는 부분이 있었다. 그래서 매니저님과 상담을 하고 서버를 조금더 빠르게 실행되게 하는 부분에 있어서 생각을 해보고, 그부분을 생각해보면 ci/cd를 advance로 공부해보는 것이 좋을 것 같다고 생각해서 일단 기존 코드를 고치는 작업을 하고 있다.

일단 정규표현식을 기존코드에서는 function으로 만들어서 한번, joiSchema를 이용해서 두번 검증하고 있었는데 이부분은 불필요한것 같아(프론트에서도 체크를 하기 때문에) 삭제를 했고 시간을 나타내기위해 moment와 dayjs 두 라이브러리를 비교했는데 유의미한 차이가 나지 않아 조금더 많은 사용자가 사용하는 moment를 사용했다. (dayjs가 용량이 더 가볍다고는 하는데 기존에 moment를 사용하고있어서 속도가 유의미하게 차이나지 않는 이상 변경은 필요없다고 생각이 들었다.)

보안에 대해서도 조금 생각을 하게 되서 알아보니 helmet이라는 보안 모듈이 있었다.
쓰는 방법은 아주 간단했는데

const helmet = require('helmet');
app.use(helmet());

이었다.

helmet은 여러 보안적인 기능들이 있는데 정리하자면 다음과 같다.

  • csp : Content-Security-Policy 헤더 설정 / XSS(Cross-site scripting) 공격 및 기타 교차 사이트 인젝션 예방.
  • hidePowerdBy : X-Powered-By 헤더 제거
  • hpkp : Public Key Pinning 헤더 추가. 위조된 인증서를 이용한 중간자 공격 방지.
  • hsts : SSL/TLS를 통한 HTTP 연결을 적용하는 Strict-Transport-Security 헤더 설정.
  • noCache : Cache-Control 및 Pragma 헤더를 설정하여 클라이언트 측에서 캐싱을 사용하지 않도록 함.
  • frameguard : X-Frame-Options 헤더 설정하여 clickjacking에 대한 보호 제공
  • ieNoOpen : (IE8 이상) X-Download-Options 설정.
  • xssFilter : X-XSS-Protection 설정. 대부분의 최신 웹 브라우저에서 XSS(Cross-site scripting) 필터를 사용.
  • noSniff : X-Content-Type-Options 설정하여, 선언된 콘텐츠 유형으로부터 벗어난 응답에 대한 브라우저의 MIME 가로채기를 방지.

csp / expectCt / hpkp / noCache / referrPplicy는 별도로 적용해야한다.

아직까지는 공격의 원리 등을 자세하게 파악하지 못해 필요성을 크게 느끼지 못해서 기본적인 기능만 사용하려고 한다.

소셜로그인을 해보기위해 passport-github, passport, express-session을 다운받았다.
github 로그인을 하기위해 이 블로그를 참조해서 생성은 했는데 아직 이해가 가지않는 부분이 있어서 받아서 jwt와 cookie로 생성하는 부분이 필요할 것 같다.

2021.10.13

프론트분들이 데이터 폼을 요청하셨는데 mysql에서 바로 꺼내서 쓸 수는 없어서 새로 response용 데이터를 가공하는 작업을 했다.

const todo = {
    id: todos.userId,
    date: todos.date,
    data: [
      {
        x: '완성도',
        y: todos.perfection,
      },
      {
        x: '창의성',
        y: todos.creativity,
      },
      {
        x: '난이도',
        y: todos.difficulty,
      },
      {
        x: '집중도',
        y: todos.concentration,
      },
      {
        x: '만족도',
        y: todos.satisfaction,
      },
    ],
  };
  const yesterdayTodo = {
    id: yesterdayTodo.userId,
    date: yesterdayTodos.date,
    data: [
      {
        x: '완성도',
        y: yesterdayTodos.perfection,
      },
      {
        x: '창의성',
        y: yesterdayTodos.creativity,
      },
      {
        x: '난이도',
        y: yesterdayTodos.difficulty,
      },
      {
        x: '집중도',
        y: yesterdayTodos.concentration,
      },
      {
        x: '만족도',
        y: yesterdayTodos.satisfaction,
      },
    ],
  };

이렇게 두번 가공을 해줘야 됐는데 이게 맞는지는 잘 모르겠다. 일단은 오늘의 todo에 대한 평가와 어제의 todo에 대한 평가가 필요해서 date로 두번 찾았고 두번 가공을 해줬는데 조금 더 깔끔하게 짜고 싶다는 생각이 들었다.

프론트서버와 첫 통신에서 문제가 생겼다.
로그인을 하게되면 쿠키가 프론트서버에 생성이 되어야하는데 되지않아서 검색해본결과 204로 통신이 되는건 처음에는 cors문제여서 cors 옵션을 만져주니 해결이 되었다.. 그런데 200으로 상태가 바뀌었는데도 계속해서 쿠키가 생성이 안되서 해결방법을 찾고있다..

  res.cookie('user', token, {
    maxAge: 50 * 60 * 1000,
    httpOnly: true,
    sameSite: 'none',
    domain: 'http://hanghaesherpa.s3-website.ap-northeast-2.amazonaws.com',
    Secure: true,
  });

쿠키를 만들어주는 코드

app.use(
cors({
  origin: 'http://hanghaesherpa.s3-website.ap-northeast-2.amazonaws.com',
  credentials: true,
})
);

cors 설정

해당 오류가 뜨는 네트워크
this attempt to set a cookie via a set-cookie header was blocked because its domain attribute was invalid with regards to the current host url

멘토님께 물어본 결과 쿠키를 서버에서 만들어서 억지로 application에 넣지 말고 토큰을 클라이언트로 보내서 클라이언트에서 쿠키를 생성하게 해야된다고 했다.
그래서 토큰을 주도록 하고 클라이언트에서 쿠키를 생성하게 했더니 잘 생성이 됐지만 이번에는 그걸 가져오는 부분에서 문제가 생겨서 프론트에서는 요청 헤더에 bearer에 쿠키를 담는걸 내일 연습해보고 백은 그걸 사용하는걸 해보기로 했다.

소셜로그인은 아무래도 쿠키로 내가 원하는방식대로 가공하기에는 조금 무리가 있어 보여서 테스트코드를 짜기로 했다. db에 문제가 생겼을 때 next(err)를 호출하기 위해서

User.findOne.mockReturnValue(Promise.reject(err));

를 썼는데

PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

라는 에러가 발생했다.

해결.
Promise를 쓰는대신 항상 reject를 반환하는

await User.findOne.mockRejectedValue(err);

를 사용하여 reject를 발생시켜서 next(err)를 호출했다.

2021-10-14

오늘 하루는 이것저것 조금씩 겉핥기하는 하루였던것 같다.

bearer에 쿠키를 담아주는 방식으로 하여 loginAuth 부분을 다음과 같이 변경했다.

async (req, res, next) => {
  const { authorization } = req.headers;
  if (authorization === undefined) {
    return res.status(401).json({});
  }
  const [bearer, token] = authorization.split(' ');
  try {
    const decoded = jwt.verify(token, process.env.SECRET_KEY);
    const user = await User.findOne({
      where: { userId: decoded.userId },
    });

    res.locals.user = user.id;

    console.log('로컬 유저는?', res.locals.user);

    next();
  } catch (err) {
    console.error(err);
  }
};

그리고 이외에 자잘한 오타들 등을 fix하고 하는데 하루가 걸렸고, passport에 대해서 조금 더 알아봤다. 하지만 아직까지는 제대로 깊게이해는 되지 않아서 조금 더 찾아봐야 할 것 같다.

저녁부터 통신위주로 진행이 되다보니 유지보수쪽을 좀 많이했다. 백엔드에서 정제하는부분이 있는데 그 부분에서 경우의 수를 생각해야 됐다. 우리 서비스에서는 선택한 날의 todo에 대한 정보와 전날의 todo에 대핸 정보가 두개가 내려와야되는데, 이때 4가지의 경우의 수가 발생한다.

  1. todo yesterday todo 둘다 있는경우
  2. todo 만 있는경우
  3. yesterdaytodo만 있는경우
  4. 둘다 없는경우

프론트와 이야기를 해본 결과 둘다 없는 경우에 대해서만 400을 내려주고 나머지 2번과 3번의 경우에는 없는쪽을 더미데이터로 가공해서 내려달라고 했다.

그래서 순서대로 1,2,3,4 로 했는데 둘다 없는경우에는 2번에서 걸려서(둘다 없는경우 !yesterday인 2번에서 걸려서 안내려간다..)
진행이 안되서 4번을 2,3번보다 위에 위치시켜서 진행이 되게 했다. 그 외에도 date정보를 params로 받기위해 요청을 드렸고 흔쾌히 해드렸다.
메인페이지 기능이 거의 다 끝나서 프론트분들은 퇴근(?)하러 가셨고 나는 남아서 test코드를 조금 더 짜고있다.
기능테스트코드 하나 완성한 후 부하테스트라는걸 실험해봤다.





로그인 함수에 60초동안 1초에 30번씩 요청을 보냈을 때

40초 이후부터는 실행을 못시킨다. 로직을 확인해보면

router.post('/login', async (req, res) => {
const { userId, password } = await postLoginSchema.validateAsync(req.body);
const user = await User.findOne({
where: { userId },
});
if (!bcrypt.compareSync(password, user.password)) {
return res.status(400).send({});
}
const token = jwt.sign({ userId }, process.env.SECRET_KEY);
const nickname = user.nickname;
res.status(200).json({ nickname, token });
});

되게 간단한데, 아마도 user find하는 곳에서 index 설정을 해주면 조금 좋아지지 않을까 싶다. 내일 일어나서 인덱싱을 한번 해봐야 겠다.

2021.10.15

어제에 이어 부하테스트를 했는데,
index 설정을 해도 시간이 나아지지 않길래 코드를 차근차근 본 결과 bcrypt comparesync로 진행이 되고있어서 compare로 바꾸어 주었더니 시간문제가 해결됐다.

 if (!bcrypt.comparesync(password, user.password)) {
    return res.status(400).send({});
  }

문제가 되었던 comparesync

 const compare = await bcrypt.compare(password, user.password);
  if (!compare) {
    return res.status(400).send({});
  }

콜백함수 처리를 하려다가 optional이길래 그냥 2개 인자만 주었다.

더 많은 요청을 보내도 간단하게 처리가 되는모습을 보여줬다.
초당 70번 요청을 보냈을때까지만 해도 그렇게 느려지지 않았는데 초당 한 80번정도 요청을 보내게되면 요청당 약 1.7초정도 걸리는 모습을 보여주고 100번정도 요청을 보내게 되면 timeout에러가 걸리는 모습을 보여줬다. 이부분에 있어서는 서버성능이 문제인 것 같았다.

mypage 구현을 시작해서 구현을 하려던 중 7일의 데이터에 대한 경우의 수 출력이 조금 어려워서 없는데이터를 더미데이터로 주는 것이 아니라 있는 데이터만 주기로 했다.
이를 위해서 filter를 사용하여 null인경우 배열에 넣지 않고 데이터가 있을때만 데이터를 배열에 넣기로 했다.

const today = moment().format('YYYY-MM-DD');
  const user = '4';
  const [year, month, days] = today.split('-');
  const day = [];
  for (let i = 0; i < 7; i++) {
    const dayMinus = String(Number(days) - i);
    day.push(dayMinus);
  }
  const dayM1 = `${year}-${month}-${day[1]}`;
  const dayM2 = `${year}-${month}-${day[2]}`;
  const dayM3 = `${year}-${month}-${day[3]}`;
  const dayM4 = `${year}-${month}-${day[4]}`;
  const dayM5 = `${year}-${month}-${day[5]}`;
  const dayM6 = `${year}-${month}-${day[6]}`;

  const todosM0 = await Todo.findOne({ where: { date: today, user } });
  const todosM1 = await Todo.findOne({ where: { date: dayM1, user } });
  const todosM2 = await Todo.findOne({ where: { date: dayM2, user } });
  const todosM3 = await Todo.findOne({ where: { date: dayM3, user } });
  const todosM4 = await Todo.findOne({ where: { date: dayM4, user } });
  const todosM5 = await Todo.findOne({ where: { date: dayM5, user } });
  const todosM6 = await Todo.findOne({ where: { date: dayM6, user } });

  if (
    !todosM0 &&
    !todosM1 &&
    !todosM2 &&
    !todosM3 &&
    !todosM4 &&
    !todosM5 &&
    !todosM6
  ) {
    return res.status(400).json({});
  }

  const todoArr = [
    todosM0,
    todosM1,
    todosM2,
    todosM3,
    todosM4,
    todosM5,
    todosM6,
  ];
  const weakTodoArr = [];
  todoArr.filter((val, idx) => {
    if (todoArr[idx] !== null) {
      return weakTodoArr.push({
        id: todoArr[idx].user,
        date: todoArr[idx].date,
        data: [
          {
            x: '완성도',
            y: todoArr[idx].perfection,
          },
          {
            x: '창의성',
            y: todoArr[idx].creativity,
          },
          {
            x: '난이도',
            y: todoArr[idx].difficulty,
          },
          {
            x: '집중도',
            y: todoArr[idx].concentration,
          },
          {
            x: '만족도',
            y: todoArr[idx].satisfaction,
          },
        ],
      });
    }
  });

여기서 아쉬운 점은 뭔가 더 깔끔하게 코드를 줄일 수 있었다고 생각이 드는데 못한점이 조금 아쉬워서 나중에 리팩토링 할 때 한번 생각을 해봐야겠다고 생각했다.

2021-10-16

마지막날이라서 서버와 통신하는 부분만 하고 나머지는 할게 많이 없다. 멘토님한테 스코프는 적당했지만 아이디어가 너무 참신해서 좋았다고 한다.

백엔드 깃허브 주소
https://github.com/changchanghwang/hanghaeSherpa_back_end

멘토링이 끝나고 다같이 모여서 프론트는 백에게 프론트코드를 설명해주고 백은 프론트한테 백 코드를 설명해줘서 서로 약간 알아가는 과정을 했다.

2021-10-17

백엔드 코드를 살짝 수정했다. 날짜를 구현하는 로직에서 내가 너무 안일했다. moment라는 라이브러리를 쓰면서 제대로 쓰지 않고 내가 따로 날짜를 수정하는 로직을 구현했는데, 이때 월 초일일때 어제날짜와 일주일치 날짜를 구하는 로직을 구현하지 않았던것이다. 그래서 그부분에 있어서 moment를 이용해서 수정했다.

const days = date.split('-');
  const day = String(Number(days['2']) - 1);
  const yesterday = `${days[0]}-${days[1]}-${day}`;

이렇게 그냥 너무나도 단순하게 생각했는데
1일일 경우 0으로 되는 경우를 생각을 아예 못하고 있었다.

  const yesterday = moment(date).subtract(1, 'day').format('YYYY-MM-DD');

그래서 모멘트라이브러리를 사용하여 코드도 짧아지고 더 좋은 코드를 구현했다.

그리고 mypage에서 filter를 사용해서 배열을 재조합하는것 보다 reduce를 사용하는 것이 조금 더 짧은 코드가 구현되는 것 같아서 reduce를 사용했다.

weekTodoArr = [];
  없는 데이터를 빼주고 있는데이터는 가공해서 새로운 배열에 push
  todoArr.filter((val, idx) => {
    if (todoArr[idx] !== null) {
      return weekTodoArr.push({
        id: todoArr[idx].date,
        data: [
          {
            x: '완성도',
            y: todoArr[idx].perfection,
          },
          {
            x: '창의성',
            y: todoArr[idx].creativity,
          },
          {
            x: '난이도',
            y: todoArr[idx].difficulty,
          },
          {
            x: '집중도',
            y: todoArr[idx].concentration,
          },
          {
            x: '만족도',
            y: todoArr[idx].satisfaction,
          },
        ],
      });
    }
  });

원래 코드

weekTodoArr = todoArr.reduce((prev, cur) => {
    if (cur !== null) {
      prev.push({
        id: cur.date,
        data: [
          {
            x: '완성도',
            y: cur.perfection,
          },
          {
            x: '창의성',
            y: cur.creativity,
          },
          {
            x: '난이도',
            y: cur.difficulty,
          },
          {
            x: '집중도',
            y: cur.concentration,
          },
          {
            x: '만족도',
            y: cur.satisfaction,
          },
        ],
      });
    }
    return prev;
  }, []);

바꾼코드

이번 협업에서

사실 이번 협업에서 프론트분들에게나 백엔드 분에게나 좀 디테일한 면을 많이들 요구했는데,

예를 들면 이런식으로...
백엔드 분한테는 직접 말로 요구했던것 같다.. 가령 로그인 로직에서 joi스키마를 요구하거나 미들웨어에서 토큰을 받는 로직을 구현하라고 하거나 말을 했다..

프론트분들도 그렇고 백엔드분도 그렇고 매우매우 착하게 요구사항을 다 들어주시고 하셔서 너무나도 감사했다.

0개의 댓글