Docker로 MongoDB를 시작하여 Heroku가 디버깅할 때까지 개발

개시하다


나는 전방에서 Docker를 별로 사용하지 않으려고 하지만 DB 서버를 준비하려면 Docker를 사용하면 프로그램을 설치할 필요가 없어 편리해진다.
그래서 몬godB가 Docker로 개발을 시작한 과정을 보도하고 싶습니다.
그리고 이번에 만든 소스는 여기에 두세요.
https://github.com/wintyo/mongodb-vue-todo

사용된 기술

  • Docker
  • MongoDB
  • express
  • mongoose
  • TypeScript
  • Vue.js3계
  • Heroku
  • MongoDB 서버 시작


    Docker 설치


    아직 Docker가 설치되지 않은 사람은 이쪽을 참고하여 설치하세요.
    https://qiita.com/nemui_/items/ed753f6b2eb9960845f7

    docker-compose로mongoDB 환경 구축


    몬godb 디렉터리에 다음 코드가 적힌 docker-compose.yml를 놓으세요.
    간단하게 말하면 mongomongo-express가 시작하고 mongo몬goDB 서버에 시작하고 mongo-expressGUI로 조작하는 서버를 시작한다.
    서로 연결하기 위해username과password를 결합시켜야 하기 때문에 환경 변수에서 접근할 수 있습니다.
    mongodb/docker-compose.yml
    version: '3.1'
    
    services:
      mongo:
        image: mongo:4.4.5
        environment:
          MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USERNAME}
          MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD}
        ports:
          - 27017:27017
        volumes:
          - ./db:/data/db
          - ./configdb:/data/configdb
    
      mongo-express:
        image: mongo-express:0.54
        ports:
          - 8081:8081
        environment:
          ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGODB_USERNAME}
          ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGODB_PASSWORD}
    
    환경 변수는 같은 디렉토리에 파일 이름.env으로 구성되어 있으면 자동으로 읽을 수 있습니다.
    mongodb/.env
    MONGODB_USERNAME=root
    MONGODB_PASSWORD=password
    
    파일을 배치한 후 mongodb 디렉터리로 이동하고 아래 명령을 누르면 시작합니다.
    $ docker-compose up -d
    
    시작 후 방문localhost:8081 시 다음과 같은 화면이 나타나며 GUI를 통해 MongoDB의 내용을 확인할 수 있다.

    참고 자료
    https://qiita.com/mistolteen/items/ce38db7981cc2fe7821a

    Mongoose에서 MongodB 조작


    express 서버 설정


    우선express 서버의 프레임워크를 만듭니다.API는 server/apis 디렉토리에서 관리되므로 import을 직접 사용합니다.
    server/server.ts
    import Express from 'express';
    import http from 'http';
    import path from 'path';
    
    import apiRouter from './apis/';
    
    const app = Express();
    const port = process.env.PORT || 9000;
    const server = http.createServer(app);
    
    app.use(Express.json());
    app.use(Express.urlencoded({ extended: true }));
    
    // static以下に配置したファイルは直リンクで見れるようにする
    app.use(Express.static(path.resolve(__dirname, 'static')));
    
    // APIの設定
    app.use('/api', apiRouter);
    
    server.listen(port, () => {
      console.log(`Listening on port ${port}`);
    });
    
    server/apis/index.ts는 다음과 같다.이번엔 그 정도는 아니어도 괜찮지만 앞으로 규모가 커질 것을 고려해서요.
    server/apis/index.ts
    import Express from 'express';
    // この後作る
    // import todoRouter from './router/todos';
    
    const router = Express.Router();
    
    // サーバーの動作確認
    router.get('/health', (req, res) => {
      res.send('I am OK!');
    });
    
    // ルーティングの設定
    // router.use('/todos', todoRouter);
    
    export default router;
    
    일단 방문locahost:9000/api/health하면,이런 문자열이 바뀌었다.

    Mongoose를 통해 MongoDB 액세스

    server/server.ts에 다음 코드를 추가하여 MongoDB에 접근합니다.접근할 때username과password가 필요하기 때문에 dotenv 환경 변수 파일을 읽는 파일을 조합합니다.
    server/server.ts
    import mongoose from 'mongoose';
    
    // 本番じゃない時はローカルのDBに接続する
    if (process.env.NODE_ENV !== 'production') {
      require('dotenv').config({ path: path.resolve(__dirname, '../mongodb/.env') });
    
      const { MONGODB_USERNAME, MONGODB_PASSWORD } = process.env;
      mongoose.connect('mongodb://localhost:27017', {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        user: MONGODB_USERNAME,
        pass: MONGODB_PASSWORD,
        dbName: 'todo-app',
      })
        .catch((err) => {
          console.error(err);
        });
    }
    

    모델 정의


    그런 다음 Todo 데이터의 모델을 정의합니다.어렵게 타입 스크립트를 만들었기 때문에 정적 모형도 잘 만들고 싶었는데 생각보다 힘들었어요.마지막으로 기본적으로 자신의 유형 정의가 되었다.ITodoFields는 기본 데이터 형식으로timestamps 옵션createdAt과 업데이트dAt`를 추가합니다.이것은 진짜와 가짜를 전환해야 하지만 설정이 어려워서 모두 진짜 설정의 구상에 따라 진행된다.
    server/model/Todo.ts
    import mongoose from 'mongoose';
    
    interface ITimestamps {
      createdAt: Date;
      updateAt: Date;
    }
    
    type tDocument<Fields> = Fields & ITimestamps & mongoose.Document;
    type tSchema<Fields> = {
      [K in keyof Fields]: mongoose.SchemaDefinitionProperty<Fields[K]>;
    };
    
    interface ITodoFields {
      isDone: boolean;
      text: string;
    }
    
    const todoSchema: tSchema<ITodoFields> = {
      isDone: { type: Boolean, default: false },
      text: String,
    }
    
    export default mongoose.model<tDocument<tSchema<ITodoFields>>>(
      'Todo',
      new mongoose.Schema(todoSchema, { timestamps: true })
    );
    
    참고 자료
  • mongoose 모델 및 schema에서 Type Script로 정형
  • Mongoose the Typescript way…?
  • API 정의 및 DB 작업


    그런 다음 이 모델을 사용하여 각 API에 적절한 작업을 적습니다.
    server/apis/router/todos.ts
    import Express from 'express';
    import TodoModel from '../../models/Todo';
    
    const router = Express.Router();
    
    router.route('/')
      .get((req, res) => {
        TodoModel
          .find()
          .sort({ createdAt: -1 })
          .then((todos) => {
            res.json(todos);
          })
          .catch((err) => {
            res.status(500).send(err);
          });
      })
      .post((req, res) => {
        const { text } = req.body;
    
        const todo = new TodoModel();
        todo.text = text;
        todo.save((err) => {
          if (err) {
            res.status(500).send(err);
            return;
          }
          res.send('ok');
        });
      });
    
    router.route('/:todoId')
      .delete((req, res) => {
        const { todoId } = req.params;
        TodoModel
          .deleteOne({ _id: todoId })
          .then(() => {
            res.send('ok');
          })
          .catch((err) => {
            res.status(500).send(err);
          });
      });
    
    router.route('/check/:todoId')
      .put((req, res) => {
        const { todoId } = req.params;
        const { isDone } = req.body;
    
        TodoModel
          .findOne({ _id: todoId })
          .then((todo) => {
            if (todo == null) {
              res.status(400).send(`Todo (id: ${todoId}) not found.`);
              return;
            }
            todo.isDone = isDone;
            todo.save((err) => {
              if (err) {
                res.status(500).send(err);
                return;
              }
              res.send(todo);
            });
          })
          .catch((err) => {
            res.status(500).send(err);
          });
      });
    
    export default router;
    
    API를 만든 후 curl로 전송해 보십시오.todo-app 데이터베이스를 구축했는데 todos 소장품에 데이터가 하나 있으면 성공한다.
    $ curl -X POST -H "Content-Type: applicati
    on/json" -d '{"text":"test todo"}' localhost:9000/api/todos
    

    TODO 리스트 화면 제작


    API가 완성되면 화면이 완성됩니다.여기도 타입 스크립트로 쓸 수 있지만 이번에는 서버 위주여서 프론트에서 index를 간단하게 사용할 수 있다.에 불과하다.
    맨 위에 있는 Todo를 삭제하면 애니메이션이 순조롭게 진행되지 않습니다. 신경 쓰이지만 당분간은 가능합니다.(Vue.js2학과 때는 괜찮아요.)
    index.html
    <!DOCTYPE html>
    <html lang="ja">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>MongoDBを使ったTODOリスト</title>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.4/vue.global.js"></script>
      <script src="https://unpkg.com/vue-types"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
      <style>
        * {
          box-sizing: border-box;
        }
    
        .list {
          position: relative;
          padding: 0;
        }
    
        .item {
          position: relative;
          display: flex;
          align-items: center;
          width: 100%;
          transition: all 0.5s;
          padding: 10px;
          margin-top: 10px;
          border: solid 1px #ddd;
          border-radius: 5px;
          box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
          cursor: pointer;
        }
    
        .item__text {
          flex: 1 1 0;
          padding: 0 5px;
        }
    
        /* 要素が入るときのアニメーション */
        .flip-enter-from {
          opacity: 0;
          transform: translate3d(0, -30px, 0);
        }
        .flip-enter-to {
          opacity: 1;
          transform: translate3d(0, 0, 0);
        }
    
        /* 要素が消える時のアニメーション */
        .flip-leave-active {
          position: absolute;
        }
        .flip-leave-from {
          opacity: 1;
          transform: translate3d(0, 0, 0);
        }
        .flip-leave-to {
          opacity: 0;
          transform: translate3d(0, -30px, 0);
        }
      </style>
    </head>
    <body>
      <div id="app"></div>
      <script>
        const { reactive, onBeforeMount } = Vue;
    
        // APIリクエストの設定
        const api = axios.create({
          baseURL: '/api',
          timeout: 15000,
        });
    
        /**
         * 入力フォームコンポーネント
         */
        const InputForm = {
          template: `
            <form @submit="onSubmit">
              <input v-model="state.text" type="text" placeholder="TODO" />
              <button type="submit" :disabled="state.text === ''">送信</button>
            </form>
          `,
          emits: {
            submit: (text) => {
              return text != null;
            },
          },
          setup(props, context) {
            const state = reactive({
              text: '',
            });
    
            return {
              state,
              /**
               * 送信時
               * @param {event} event - DOMのイベント
               */
              onSubmit(event) {
                event.preventDefault();
                context.emit('submit', state.text);
                state.text = '';
              },
            };
          },
        };
    
        /**
         * TODOリストコンポーネント
         */
        const TodoList = {
          template: `
            <transition-group tag="ul" class="list" name="flip">
              <template
                v-for="item in $props.todoList"
                :key="item._id"
              >
                <li
                  class="item"
                  @click="$emit('check', item._id, !item.isDone)"
                >
                  <input type="checkbox" :checked="item.isDone" />
                  <span class="item__text">{{ item.text }}</span>
                  <button @click="onDeleteTodo($event, item._id)">削除</button>
                </li>
              </template>
            </transition-group>
          `,
          emits: {
            check: (todoId, isChecked) => {
              return todoId != null && isChecked != null;
            },
            delete: (todoId) => {
              return todoId != null;
            },
          },
          props: {
            todoList: VueTypes.arrayOf(VueTypes.shape({
              _id: VueTypes.string.isRequired,
              isDone: VueTypes.bool.isRequired,
              text: VueTypes.string.isRequired
            }).loose).isRequired
          },
          setup(props, context) {
            return {
              /**
               * 削除ボタンをクリックした時
               * @param {event} event - DOMのイベント
               * @param {number} todoId - TODOのID
               */
              onDeleteTodo(event, todoId) {
                event.stopPropagation();
                context.emit('delete', todoId);
              },
            };
          },
        };
    
        const app = Vue.createApp({
          template: `
            <div>
              <InputForm
                @submit="onSubmit"
              />
              <p>TODO LIST</p>
              <TodoList
                :todoList="state.todoList"
                @check="onCheckTodo"
                @delete="onDeleteTodo"
              />
            </div>
          `,
          components: {
            InputForm,
            TodoList,
          },
          setup() {
            const state = reactive({
              todoList: [],
            });
    
            const fetchTodoList = async () => {
              const response = await api.get('/todos');
              state.todoList = response.data;
            };
    
            onBeforeMount(async () => {
              await fetchTodoList();
            });
    
            return {
              state,
              /**
               * 送信された時
               * @param {string} text - テキスト
               */
              async onSubmit(text) {
                await api.post('/todos', {
                  text,
                });
                await fetchTodoList();
              },
              /**
               * TODOのチェック
               * @param {number} todoId - TODOのID
               * @param {boolean} isNextDone - 次更新する終了ステータス
               */
              async onCheckTodo(todoId, isNextDone) {
                await api.put(`/todos/check/${todoId}`, {
                  isDone: isNextDone,
                });
                await fetchTodoList();
              },
              /**
               * TODOの削除ボタンがクリックされた時
               * @param {number} todoId - TODOのID
               */
              async onDeleteTodo(todoId) {
                await api.delete(`/todos/${todoId}`);
                await fetchTodoList();
              },
            }
          },
        });
    
        app.mount('#app');
      </script>
    </body>
    </html>
    

    Heroku에게 프러포즈


    DB 서버 공식 사용 준비


    마지막으로 헤로쿠를 디버깅하고 그 전에 정식 공연용 DB를 준비한다.예전에는 mLab이라는 아드레날린이 있었는데 지금은 없어졌다MongoDB Atlas.
    여기 기사를 참고해서 컬렉션을 만들어 보세요.
    https://qiita.com/n0bisuke/items/4d4a4599ee7ce9cf4fd9
    제작이 완료되면 연결 정보가 표시되면 여기의 URI를 복사합니다.그러나 비밀번호<password>가 포함되지 않기 때문에 설정된 비밀번호로 바꿔야 한다.

    URI 취득 후 다음 연결 몬godB 연결 방법을 전환할 수 있는지 확인합니다.
    server/server.ts
    // 一旦本番DBに接続する
    if (process.env.NODE_ENV !== 'production') {
      const MONGODB_URI = '<password>を書き換えてコピーしたURI';
      mongoose.connect(MONGODB_URI, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        dbName: 'todo-app',
      })
        .catch((err) => {
          console.error(err);
        });
    }
    

    Heroku 환경 변수의 설정과 디자인


    Heroku 응용 프로그램을 만들면 Settings 페이지에 환경 변수를 추가합니다.
    이름은 무엇이든지 가능하지만 여기MONGODB_URI의 이름으로 저장합니다.

    이 환경 변수를 사용하여 액세스할 코드를 덮어씁니다.
    server/server.ts
     // 本番じゃない時はローカルのDBに接続する
     if (process.env.NODE_ENV !== 'production') {
       require('dotenv').config({ path: path.resolve(__dirname, '../mongodb/.env') });
    
       const { MONGODB_USERNAME, MONGODB_PASSWORD } = process.env;
       mongoose.connect('mongodb://localhost:27017', {
         useNewUrlParser: true,
         useUnifiedTopology: true,
         user: MONGODB_USERNAME,
         pass: MONGODB_PASSWORD,
         dbName: 'todo-app',
       })
         .catch((err) => {
           console.error(err);
         });
    -}
    +} else {
    +  mongoose.connect(process.env.MONGODB_URI || '', {
    +    useNewUrlParser: true,
    +    useUnifiedTopology: true,
    +    dbName: 'todo-app',
    +  })
    +    .catch((err) => {
    +      console.error(err);
    +    });
    +}
    
    이후 히로쿠의 디자인이 완성됐다.

    끝맺다


    이상은 Docker를 사용하여 MongoDB 서버 개발을 시작하고 Heroku 디버깅을 할 때까지 하는 절차입니다.예전에는 mLab이라는 공짜 Mongodb adion이 있었는데, Heroku를 사용하면 좋을 것 같았는데, 지금은 그 은혜를 받지 못한 것 같다.DB를 공짜로 쓰는 게 어려워요?만약 작은 데이터가 무료 파일이라면 Firebase는 더욱 쉽게 할 수 있고 DB를 일부러 사용한다는 뜻은😅
    필요하신지 모르겠는데 누구한테 도움이 됐으면 좋겠어요.

    좋은 웹페이지 즐겨찾기