IT STUDY LOG

Sprint - Cozstory WAS 개발 본문

devops bootcamp 4/pair/team log

Sprint - Cozstory WAS 개발

roheerumi 2023. 3. 28. 15:13

#해결 과제

CozStory 프론트엔드를 빌드하고, nginx를 이용해 정적 웹사이트로 호스팅하세요.

프레임워크인 fastify를 이용해 API 서버를 작성합니다.

CosStory API 문서 및 fastfiy 공식 문서를 참고해서, 요청에 따른 응답을 구현합니다.

낯선 구조를 가진 코드를 이해하고, 각 디렉토리와 파일이 어떤 구조로 짜여져있는지 이해합니다.

CozStory의 API 서버가 가진 한계를 이해하고, 영속적으로 데이터를 저장하려면 어떻게 접근해야 하는지 고민해봅니다.

잘 작성되었는지를 확인하기 위해 테스트를 실행하고, 모든 테스트케이스를 통과해야 합니다.

 

#실습 자료

 

#과제 항목별 진행 상황

💡 sprint-cozstory-was 스프린트 레포지토리를 통해 소스코드를 클론합니다. npm install 명령어로 서버 폴더에 필요한 모듈을 설치합니다. package.json을 참고해, 서버를 어떻게 실행해야 하는지 파악합니다. 디렉토리 구조를 파악하고, 각 디렉토리와 파일이 어떤 역할을 하는지 이해해봅니다.

서버에 필요한 모듈 설치 및 package.json 확인

$ npm istall
$ cat package.json 
{
  "name": "3tier",
  "version": "1.0.0",
  "description": "This project was bootstrapped with Fastify-CLI.",
  "main": "app.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "NODE_ENV=test tap -Rspec test/**/*.test.js",
    "start": "fastify start -l info app.js",
    "dev": "fastify start -w -l info -P app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "tap": {
    "nyc-arg": [
      "--exclude=model/**/*.js",
      "--exclude=plugins/mongodb.js"
    ]
  },
  "dependencies": {
    "@fastify/autoload": "^5.4.1",
    "@fastify/cors": "^8.2.0",
    "@fastify/sensible": "^5.1.1",
    "ajv": "^8.11.2",
    "dotenv": "^10.0.0",
    "fastify": "^4.9.2",
    "fastify-cli": "^2.15.0",
    "fastify-plugin": "^4.3.0",
    "sinon": "^12.0.1"
  },
  "devDependencies": {
    "tap": "^15.0.9"
  }
}

디렉터리 구조

  1. model 디렉터리
    1. scheme.js : 객체 타입 정의 추정
    2. index.js : 실질적인 CRUD 메서드 구현
  2. node modules 디렉터리 : npm install 시 필요한 모듈들이 위치
  3. routes 디렉터리 : API Controller 역할로 요청, 응답 URL 매핑
  4. plugins 디렉터리 : 필요한 plugins 위치
  5. test 디렉터리 : 테스트 관련 파일 작성
  6. app.js 파일 : 메인 애플리케이션
  7. package-lock.json 파일: npm이 설치한 모든 패키지와 해당 종속성 버전을 정확하게 기록해 버전 충돌을 방지하고 일관된 패키지 설치를 보장
  8. package.json 파일 : 프로젝트에 필요한 내용(앱명, 버전, 설명 의존성 등) 정의

💡 서버 코드 작성

조회 API 문서

read.js

// 읽기 routes/article/read.js

'use strict'

const { readAll, readOne } = require('../../model')

module.exports = async function (app, opts) {
  app.get('/', async function (request, reply) {
    const result = await readAll()

    reply
      .code(200)
      .header('Content-type', 'application/json')
      .send(result)
  })

  app.get('/:id', async function (request, reply) {
    // console.log(request);
    const result = await readOne(request.params.id)
    console.log(result);
    if(result) {
      reply
        .code(200)
        .header('Content-type', 'application/json')
        .send(result)    
    } else {
      reply
        .code(404)
        .header('Content-type', 'application/json')
        .send("Not Found")
    }
  })
}

구동 화면


생성 API 문서

create.js

// 쓰기 routes/article/create.js
// 응답코드 201, 400
'use strict'

const { createOne, isValid } = require('../../model')

module.exports = async function (app, opts) {
  app.post('/', async function (request, reply) {
    if(!isValid(request.body)) {
      reply
        .code(400)
        .header('Content-type', 'application/json')
        .send()
      return;
    }

    const result = await createOne(request.body)
    if (result) {
      reply
      .code(200)
      .header('Content-type', 'application/json')
      .send(result)
    } else {
      reply
      .code(404)
      .header('Content-type', 'application/json')
      .send(result)
    }
  })
}

구동 화면


삭제 API 문서

delete.js

// 삭제 routes/article/delete.js
'use strict'

const { deleteOne } = require('../../model')

module.exports = async function (app, opts) {
  app.delete('/:id', async function (request, reply) {
      const result = await deleteOne(request.params.id)
      console.log(result);
      if(result) {
        reply
          .code(200)
          .header('Content-type', 'application/json')
          .send(result)    
      } else {
        reply
          .code(204)
          .header('Content-type', 'application/json')
          .send(result)
      }
    })
}

수정 API 문서

update.js

// 수정 routes/article/update.js
'use strict'

const { updateOne, isValid } = require('../../model')

module.exports = async function (app, opts) {
  app.put('/:id', async function (request, reply) {
    if(!isValid(request.body)) {
      reply
        .code(400)
        .header('Content-type', 'application/json')
        .send()
      return;
    }

    const result = await updateOne(request.params.id, request.body)
    console.log(result);
    if(result) {
      reply
      .code(200)
      .header('Content-type', 'application/json')
      .send(result)    }
    else {
      reply
      .code(404)
      .header('Content-type', 'application/json')
      .send(result)
    }

  })
}

구동 화면


index.js

// model/index.js에 작성된 코드 이해
const validate = require('./scheme')()

let allArticles = [
  { "_id": 1, "author": { "name": "임푸라" }, "title": "나는 인프라 담당자", "body": "데브옵스는 재미있어" },
  { "_id": 2, "author": { "name": "김코딩" }, "title": "데브옵스도 코딩 능력이 필요할까?", "body": "당연합니다!" },
  { "_id": 3, "author": { "name": "최해커" }, "title": "서버 다운이 지겨우신가요?", "body": "가용성을 높입시다!" },
  //{ "_id": 4, "author": { "name": "wm" }, "title": "안녕", "body": "하이" },
  //{ "_id": 5, "author": { "name": "rr" }, "title": "하이", "body": "안녕" }
]

module.exports = {
  readAll: async () => {
    return allArticles;
  },
  readOne: async (id) => {
    const found = allArticles.filter(article => article._id === Number(id))[0];
    return found;
  },
  createOne: async (body) => {
    const newArticle = {
      _id: allArticles.length + 1,
      ...body
    }

    allArticles.push(newArticle)

    return newArticle;
  },
  updateOne: async (id, body) => {
    const found = allArticles.filter(article => article._id === Number(id))[0];
    console.log(found)
    if(found) {
      found.author = body.author;
      found.title = body.title;
      found.body = body.body;

      return found;
    }
  },
  deleteOne: async (id) => {
    const found = allArticles.filter(article => article._id === Number(id))[0];
    if(found) {
      const idx = allArticles.indexOf(found)
      allArticles = [...allArticles.slice(0, idx), ...allArticles.slice(idx + 1)]
    }

    return found;
  },
  isValid: (body) => {
    const valid = validate(body);
    // console.log(validate.errors)
    return valid
  }
}

💡 npm test를 통해 테스트케이스를 확인하고, API에 맞는 CRUD 작업을 위한 서버 코드를 작성하여 테스트를 통과합니다.

$ npm test

> 3tier@1.0.0 test
> NODE_ENV=test tap -Rspec test/**/*.test.js

test/routes/create.test.js
  POST /article
    ✓ should be equivalent

  POST /article 요청 시 본문이 없는 경우
    ✓ should be equivalent

    {
      _id: 1,
      author: { name: '임푸라' },
      title: '나는 인프라 담당자',
      body: '데브옵스는 재미있어'
    }
test/routes/delete.test.js
  DELETE /article/:id
    ✓ should be equivalent
    undefined

  DELETE /article/:id 요청 시 id에 해당하는 글이 없는 경우
    ✓ should be equivalent

test/routes/read.test.js
  GET /article
    ✓ should be equivalent
    {
      _id: 2,
      author: { name: '김코딩' },
      title: '데브옵스도 코딩 능력이 필요할까?',
      body: '당연합니다!'
    }

  GET /article/:id
    ✓ should be equivalent
    undefined

  GET /article/:id 요청 시 id에 해당하는 글이 없는 경우
    ✓ should be equivalent

test/routes/root.test.js
  default root route
    ✓ should be equivalent

    {
      _id: 1,
      author: { name: '임푸라' },
      title: '나는 인프라 담당자',
      body: '데브옵스는 재미있어'
    }
    {
      _id: 1,
      author: { name: '김구름' },
      title: '나는 클라우드 전문가',
      body: '구름은 나의 것'
    }
test/routes/update.test.js
  PUT /article/:id
    ✓ should be equivalent
    undefined
    undefined

  PUT /article/:id 요청 시 id에 해당하는 글이 없는 경우
    ✓ should be equivalent

  PUT /article/:id 요청 시 본문이 없는 경우
    ✓ should be equivalent

  11 passing (5s)
ERROR: Coverage for lines (98.33%) does not meet global threshold (100%)
ERROR: Coverage for branches (91.66%) does not meet global threshold (100%)
ERROR: Coverage for statements (98.33%) does not meet global threshold (100%)
------------------------------------|---------|----------|---------|---------|-------------------
File                                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------------------------------|---------|----------|---------|---------|-------------------
All files                           |   98.33 |    91.66 |     100 |   98.33 |                   
 sprint-cozstory-was                |     100 |      100 |     100 |     100 |                   
  app.js                            |     100 |      100 |     100 |     100 |                   
 sprint-cozstory-was/plugins        |     100 |      100 |     100 |     100 |                   
  cors.js                           |     100 |      100 |     100 |     100 |                   
  sensible.js                       |     100 |      100 |     100 |     100 |                   
 sprint-cozstory-was/routes         |     100 |      100 |     100 |     100 |                   
  root.js                           |     100 |      100 |     100 |     100 |                   
 sprint-cozstory-was/routes/article |   97.77 |    91.66 |     100 |   97.77 |                   
  create.js                         |      90 |       75 |     100 |      90 | 22                
  delete.js                         |     100 |      100 |     100 |     100 |                   
  index.js                          |     100 |      100 |     100 |     100 |                   
  read.js                           |     100 |      100 |     100 |     100 |                   
  update.js                         |     100 |      100 |     100 |     100 |                   
------------------------------------|---------|----------|---------|---------|-------------------

 

#Action Items

💡 만약 계속해서 서비스를 제공/사용하고 싶다면 어떻게 해야할까요?

→ 서버를 백그라운드 서비스로 기동해 터미널이 종료되어도 계속 실행되도록 (nohup) 설정

💡 다시 서버를 실행해보세요. 데이터가 그대로 남아있나요? 사라졌다면, 왜 사라졌나요? 이 문제는 어떻게 해결할 수 있을까요?

→ 하드코딩된 데이터이므로 DB 서버를 새로 구축해 데이터를 따로 관리하여 해결

 

#TROUBLE SHOOTING LOG

📝 문제 1 : WAS 서버가 올라가지 않음

  • 원인 : node 버전이 너무 낮아서 안된 것으로 생각됨
  • 해결 방안
  1. sudo apt-get remove nodejs: nodejs 삭제
  2. sudo apt-get remove npm : npm 삭제
  3. 관련 데이터 삭제
sudo apt-get remove nodejssudo apt-get remove npmsudo rm -rf /usr/local/bin/npm /usr/local/share/man/man1/node* /usr/local/lib/dtrace/node.d ~/.npm ~/.node-gyp /opt/local/bin/node /opt/local/include/node /opt/local/lib/node_modules

sudo rm -rf /usr/local/lib/node*

sudo rm -rf /usr/local/include/node*

sudo rm -rf /usr/local/bin/node*
  1. node -v : node 설치 여부 확인
  2. npm -v : npm 설치 여부 확인
  3. nvm 설치
wget -qO- <https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh> | bash
sudo apt update
sudo apt install wget
  1. nvm --version : nvm 설치 확인 (터미널을 다시시작한 후 확인)
  2. nvm install --lts : node.js 설치
  3. node -v : node 설치 확인
  4. node_modules 제거 후 다시 npm install 후 npm start

 

📝 문제 2 : 게시글 수정 후 이름이 익명으로 변경되는 문제

  • 원인 : 클라이언트와 서버간 데이터를 넘겨줄 때 작성자명 데이터가 넘겨지지 않음
  • 해결 방안
// 백엔드 index.js 수정

  updateOne: async (id, body) => {
    const found = allArticles.filter(article => article._id === Number(id))[0];
    console.log(found)
    if(found) {
      found.author = body.author; //추가
      found.title = body.title;
      found.body = body.body;

      return found;
    }
// 클라이언트 Write.js 수정
import { Link, useParams, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { useSWRConfig } from "swr";
import Navbar from "./Navbar";
import { readOne } from "./api/read";
import { create } from "./api/create";
import { update } from "./api/update";

export default function Write({ label = '새 글 쓰기' }) {
  const params = useParams()
  const navigate = useNavigate();
  const { mutate } = useSWRConfig()
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')
  const [author, setAuthor] = useState('') // 추가
  console.log(author);

  useEffect(() => {
    if(params.id) {
      readOne(params.id)()
      .then(article => {
        setTitle(article.title)
        setBody(article.body)
        setAuthor(article.author) // 추가
      })
    }
  }, [params.id])

  if(params.id) {
    label = '글 수정'
  }

  const changeBodyHandler = e => setBody(e.target.value)
  const changeTitleHandler = e => setTitle(e.target.value)
  const changeAuthorHandler = e => setAuthor(e.target.value) // 추가

  const submitHandler = async () => {

    if (params.id) {
      const result = await mutate(`update-${params.id}`, update(params.id, {
        title,
        body,
        author, // 추가
        lastUpdated: new Date().toISOString()
      }))

      console.log(result)
      if (result) {
        navigate('/')
      }
    }
    else {
      const coverImages = [
        '<https://images.unsplash.com/photo-1640781966832-cf37029e0fac?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3270&q=80>',
        '<https://images.unsplash.com/photo-1593642533144-3d62aa4783ec?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3269&q=80>',
        '<https://images.unsplash.com/photo-1504711434969-e33886168f5c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3270&q=80>'
      ]

      const result = await mutate('create', create({
        author: {
          name: 'rr&wm',
          picture: '<https://avatars.githubusercontent.com/u/702622?v=4>'
        },
        title,
        body,
        coverImage: coverImages[Math.floor(Math.random() * coverImages.length)],
        lastUpdated: new Date().toISOString()
      }))

      console.log(result)
      if (result) {
        navigate('/')
      }
    }
  }

// ... 하략 ... 

 

#References

  • fastify 요청 Request 객체 document
  • fastify 응답 Reply 객체 document

'devops bootcamp 4 > pair/team log' 카테고리의 다른 글

Sprint - Proxy Server  (0) 2023.04.07
Sprint - 로그 파이프라인  (0) 2023.03.30
Mini WAS 개발 hands-on  (0) 2023.03.28
CozStory 클라이언트 호스팅  (0) 2023.03.27
nginx Web Server Hands-on  (0) 2023.03.24
Comments