Dwitter 토이 프로젝트 – (1) ✏️

app.js의 기본 뼈대 잡기!

우선 시작 전 필요한 API 들에 대해 정리한다.

  • API 정의
    • API spec : https://www.notion.so/API-Spec-Tweets-b04541cf06f84b83bf4891abfaa27d03
    • postman : https://documenter.getpostman.com/view/9223369/UVsJvSME

필요한 미들웨어 / 라우터를 다 연결해준다.

import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
import helmet from 'helmet';
import 'express-async-errors'; // express 5 이전 비동기 에러 캐치용

const app = express();

app.use(express.json()); // body json 파싱
app.use(morgan('tiny')); // 호출 로그 미들웨어
app.use(cors()); // CORS 처리
app.use(helmet()); // CORS 보안성 보완

app.use('/tweets',tweetsRoute); // 라우팅 등록

app.use((req,res,next) => {
    res.sendStatus(404); // 없는 경로를 호출 했을 때 처리
}); 

app.use((error,req,res,next) => {
    console.error(error); // 에러 핸들링 - 최후 방어막 ㅎ_ㅎ
    res.sendStatus(500);   
});
app.listen(8080);

Tweets REST APIs – CRUD 뼈대 구성

라우터 (tweets.js)

import express from 'express';
import 'express-async-errors';

let tweets = [
    {
        id: '1',
        text: 'Hello world!',
        createAt: Date.now().toString(),
        name: 'Dylan',
        username: 'dylan',
        url: 'https://cdn-icons-png.flaticon.com/512/3576/3576887.png'
    },
    {
        id: '2',
        text: 'Hello world!',
        createAt: Date.now().toString(),
        name: 'Bob',
        username: 'bob',
        url: 'https://cdn-icons-png.flaticon.com/512/3576/3576887.png'
    },    
]; // 우선은 메모리에 저장

const router = express.Router();

// GET /tweets
// GET /tweets?username=:username
router.get('/',(req,res,next) => {
    const username =  req.query.username;
    const data = username
    ? tweets.filter(tweet => tweet.username === username)
    : tweets;
    res.status(200).json(data);
});

// GET /tweets/:id
router.get('/:id',(req,res,next) => {
    const id = req.params.id;
    const tweet = tweets.find(tweet=> tweet.id === id);
    if(tweet){
        res.status(200).json(tweet);
    }else {
        res.status(404).json({message : `Tweet id(${id}) not found`});
    }
});

// POST /tweets
router.post('/',(req,res,next) => {
    const {text, name, username} = req.body;
    const tweet = {
        id : Date.now().toString(),
        text,
        createAt : new Date(),
        name,
        username,
    }
    tweets = [tweet, ...tweets];
    res.status(201).json(tweet);
});

// PUT /tweets/:id
router.put('/:id',(req,res,next) => {
    const id = req.params.id;
    const text = req.body.text;
    const tweet = tweets.find(tweet => tweet.id === id);
    if(tweet){
        tweet.text = text;
        res.status(200).json(tweet);
    }else {
        res.status(404).json({message : `Tweet id(${id}) not found`});
    }
});

// DELETE /tweets/:id
router.delete('/:id',(req,res,next) => {
    const id = req.params.id;
    tweets = tweets.filter(tweet=> tweet.id !== id);
    res.sendStatus(204);
});

export default router;

Postman API runner test

포스트맨은 collection 내에 요청들을 다 테스트해볼 수 있는 환경을 제공하는데, 지금까지 만든 요청들은 서버에서 잘 처리해줬음을 알 수 있다 😁


프론트엔드 로직 (/src/service/tweet.js)

export default class TweetService {
  
  constructor(baseURL){
    this.baseURL = baseURL;
  }

  async getTweets(username) {
    const query = username ? `?username=${username}` : '';
    const response = await fetch(`${this.baseURL}/tweets${query}`,{
      method : 'GET',
      headers : {'Content-Type' : 'application/json'},
    });
    const data = await response.json();
    if(response.status !== 200){
      throw new Error(data.message);
    }
    return data;
  }

  async postTweet(text) {
    const response = await fetch(`${this.baseURL}/tweets`,{
      method : 'POST',
      headers : {'Content-Type' : 'application/json'},
      body : JSON.stringify({text, username: 'dylan', name: 'Dylan'}),
    });
    const data = await response.json();
    if(response.status !== 201){
      throw new Error(data.message);
    }
    return data;
  }

  async deleteTweet(tweetId) {
    const response = await fetch(`${this.baseURL}/tweets/${tweetId}`,{
      method : 'DELETE',
      headers : {'Content-Type' : 'application/json'},
    });
    if(response.status !== 204){
      throw new Error();
    }
  }

  async updateTweet(tweetId, text) {
    const response = await fetch(`${this.baseURL}/tweets/${tweetId}`,{
      method : 'PUT',
      headers : {'Content-Type' : 'application/json'},
      body : JSON.stringify({text}),
    });
    const data = await response.json();
    if(response.status !== 200){
      throw new Error();
    }
    return data;
  }
}

프론트엔드 코드 개선

위 프론트엔드 코드를 보면, 반복적으로 계속 사용되는 것들이 있다 🤔 (fetch, method, headers …) 이걸 하나로 묶어보자

http 클래스 생성 (/src/network/http.js)

export default class HttpClient {
    constructor(baseURL){
        this.baseURL = baseURL;
    }

    async fetch(url, options){
        const res = await fetch(`${this.baseURL}${url}`,{
            ...options,
            headers: {
                'Content-Type' : 'application/json',
                ...options.headers,
            }
        });
        let data;
        try {
            data = await res.json();
        }catch(error){
            console.error(error);
        } 
        
        if(res.status > 299 || res.status < 200){
            const message = data && data.message ? data.message : 'Something went wrong!';
            throw new Error(message);
        }
        return data;
    }
}

코드 개선 (/src/service/tweet.js)

export default class TweetService {
  
  constructor(http){
    this.http = http;
  }

  async getTweets(username) {
    const query = username ? `?username=${username}` : '';
    return this.http.fetch(`/tweets${query}`,{
      method : 'GET',
    });
  }

  async postTweet(text) {
    return this.http.fetch(`/tweets`,{
      method : 'POST',
      body : JSON.stringify({text, username: 'ellie', name: 'Ellie'}),
    });
  }

  async deleteTweet(tweetId) {
    return this.http.fetch(`/tweets/${tweetId}`,{
      method : 'DELETE',
    });
  }

  async updateTweet(tweetId, text) {
    return this.http.fetch(`/tweets/${tweetId}`,{
      method : 'PUT',
      body : JSON.stringify({text}),
    });
  }
}

아주 깔끔해졌다 🙂 !!!