IT STUDY LOG
Sprint - Cozstory WAS 개발 본문
#해결 과제
❓ CozStory 프론트엔드를 빌드하고, nginx를 이용해 정적 웹사이트로 호스팅하세요.
❓ 프레임워크인 fastify를 이용해 API 서버를 작성합니다.
❓ CosStory API 문서 및 fastfiy 공식 문서를 참고해서, 요청에 따른 응답을 구현합니다.
❓ 낯선 구조를 가진 코드를 이해하고, 각 디렉토리와 파일이 어떤 구조로 짜여져있는지 이해합니다.
❓ CozStory의 API 서버가 가진 한계를 이해하고, 영속적으로 데이터를 저장하려면 어떻게 접근해야 하는지 고민해봅니다.
❓ 잘 작성되었는지를 확인하기 위해 테스트를 실행하고, 모든 테스트케이스를 통과해야 합니다.
#실습 자료
- 레파지토리 : sprint-cozstory-was
- API 문서 : 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"
}
}
디렉터리 구조
- model 디렉터리
- scheme.js : 객체 타입 정의 추정
- index.js : 실질적인 CRUD 메서드 구현
- node modules 디렉터리 : npm install 시 필요한 모듈들이 위치
- routes 디렉터리 : API Controller 역할로 요청, 응답 URL 매핑
- plugins 디렉터리 : 필요한 plugins 위치
- test 디렉터리 : 테스트 관련 파일 작성
- app.js 파일 : 메인 애플리케이션
- package-lock.json 파일: npm이 설치한 모든 패키지와 해당 종속성 버전을 정확하게 기록해 버전 충돌을 방지하고 일관된 패키지 설치를 보장
- 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 버전이 너무 낮아서 안된 것으로 생각됨
- 해결 방안
- sudo apt-get remove nodejs: nodejs 삭제
- sudo apt-get remove npm : npm 삭제
- 관련 데이터 삭제
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*
- node -v : node 설치 여부 확인
- npm -v : npm 설치 여부 확인
- nvm 설치
wget -qO- <https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh> | bash
sudo apt update
sudo apt install wget
- nvm --version : nvm 설치 확인 (터미널을 다시시작한 후 확인)
- nvm install --lts : node.js 설치
- node -v : node 설치 확인
- 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
'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