최근 Vuejs에 관심이 생겨서 공부것을 바탕으로 간단한 채팅앱을 만들어 보았습니다.
이렇게 개발글 남기는건 처음이라... 어떻게 써야 잘쓰는건지 잘 모르겠지만...
좋게봐주셨으면합니다~^^


$ yarn global add @vue/cli // vue cli를 설치합니다.
$ vue create chat-front    // vue 프로젝트를 생성합니다.
$ cd chat-front
$ yarn serve               // 서버를 실행 시켜봅니다.
  • Vue Component Framework - Vuetify를 설치하겠습니다.
$ vue add vuetify
  - 커맨드를 실행하면 main.js
    import './plugins/vuetify';
    자동으로 추가 되는것을 확인 할 수 있습니다.

1. 로그인 페이지

./src/views/Login.vue

<template>
  <div class="inner-wrap" fluid fill-height>
    Login Page
  </div>
</template>

<script>
export default {
  name: 'Login',
  data() {
    return {
      msg: [],
      name: '',
    };
  },
};
</script>

./src/App.vue

<template>
  <v-app>
    <v-content>
      <!-- 라우터 -->
      <v-container fluid fill-height aacontainer>
        <router-view></router-view>
      </v-container>
      <!-- 라우터 -->
    </v-content>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  components: {
  },
  data() {
    return {
    };
  },
};
</script>

./src/router.js

import Vue from 'vue';
import Router from 'vue-router';
import Login from './views/Login.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'Login',
      component: Login,
    },
  ],
});

- 로그인 페이지가 나오는것을 확인하실수 있습니다.

  • 다음은 vuetify를 활용하여 Login page를 꾸며보겠습니다.
    스크린샷 2018-10-25 오전 1.17.19.png

./src/components/Login/LoginForm.vue

<template>
  <v-layout align-center justify-center>
    <v-flex xs12 sm6>
      <v-text-field
        v-model="userName"
        label="대화명"
        required
        v-on:keyup.enter="joinSubmit"
      ></v-text-field>
      <div class="text-xs-center">
        <v-btn @click="joinSubmit" round color="primary" dark>JOIN</v-btn>
      </div>
    </v-flex>
  </v-layout>
</template>

<script>
export default {
  name: 'LoginForm',
  props: ['join'],
  data() {
    return {
      userName: '',
    };
  },
  methods: {
    joinSubmit() {
      this.$emit('joinSubmit', this.userName);
    },
  },
};
</script>

./src/view/Login.vue

  • 로그인 페이지에 컴포넌트를 추가 수정 해줍니다.
<template>
  <div class="inner-wrap" fluid fill-height>
    <Loginform-component v-on:joinSubmit="joinSubmit"></Loginform-component>
  </div>
</template>

<script>
import LoginForm from '@/components/Login/LoginForm.vue';

export default {
  name: 'Login',
  data() {
    return {
    };
  },
  components: {
    'Loginform-component': LoginForm,
  },
  created() {
  },
  methods: {
    joinSubmit(userName) {
      this.$router.push(`/char-room/${userName}`);
    },
  },
};
</script>

./assets/_common.scss

  • 공통 css를 만들어 보겠습니다.
  • SCSS사용을 위해 yarn add sass-loader node-sass 모듈을 추가합니다.
/* http://meyerweb.com/eric/tools/css/reset/ 
   v2.0 | 20110126
   License: none (public domain)
*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed, 
figure, figcaption, footer, header, hgroup, 
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, 
footer, header, hgroup, menu, nav, section {
    display: block;
}
body {
    line-height: 1;
}
ol, ul {
    list-style: none;
}
blockquote, q {
    quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
    content: '';
    content: none;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}
html,body{
    overflow: hidden;
}
.inner-wrap {
  width: 100%;
}

-2018-10-22-2.14.40.png

2. 채팅방을 만들어 보겠습니다.

./src/components/Chat/MessageList.vue
./src/components/Chat/MessageForm.vue

  • 채팅방에 사용할 두개 컴포넌트 파일을 생성해줍니다.

socket.io를 활용해야 하므로 plugin을 만들어 주겠습니다.

./plugins/socketPlugin.js

import Vue from 'vue';
import io from 'socket.io-client';

const socket = io('http://localhost:3000');


const SocketPlugin = {
  install(vue) {
    vue.mixin({
    });

    vue.prototype.$sendMessage = ($payload) => {
      socket.emit('chat', {
        msg: $payload.msg,
        name: $payload.name,
      });
    };

    // 인스턴스 메소드 추가
    vue.prototype.$socket = socket;
  },
};

Vue.use(SocketPlugin);

그리고 main.js에 추가해줍니다.

import './plugins/socketPlugin';

그리고 ./src/Constant.js를 만들어줍니다. 상수선언에 활용합니다.

./src/Constant.js

export default {
  PUSH_MSG_DATA: 'pushMsgData',
};

채팅 내용을 담을 vuex store를 만들어보겠습니다.

./src/store.js는 삭제하겠습니다.
스크린샷 2018-10-25 오후 8.58.15.png

이런 구조로 만들어줍니다..ㅋ

./src/store/modules/socket.js

import Constant from '../../Constant';

const state = {
  msgDatas: [],
};

// getters
const getters = {
};

// actions
const actions = {
};

// mutations
const mutations = {
  [Constant.PUSH_MSG_DATA]: ($state, $payload) => {
    $state.msgDatas.push($payload);
  },
};

export default {
  state,
  getters,
  actions,
  mutations,
};

./src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import socket from './modules/socket';

Vue.use(Vuex);

const debug = process.env.NODE_ENV !== 'production';

export default new Vuex.Store({
  modules: {
    socket,
  },
  strict: debug,
  // plugins: debug ? [createLogger()] : []
});

./src/components/Chat/MessageList.vue

<template>
  <v-list v-auto-bottom="msgs">
    <transition-group name="list" >
      <div v-for="(msg,index) in msgs" v-bind:key="index">
        <v-list-tile>
          <v-list-tile-action>
            <span>{{msg.from.name}}</span>
          </v-list-tile-action>
          <v-list-tile-content>
            <v-list-tile-title>{{msg.msg}}</v-list-tile-title>
          </v-list-tile-content>
        </v-list-tile>
        <v-divider inset></v-divider>
      </div>
    </transition-group>
  </v-list>
</template>

<script>
export default {
  name: 'MessageList',
  props: ['msgs'],
};
</script>

<style>
.list-item {
  display: inline-block;
  margin-right: 10px;
}
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
  opacity: 0;
  transform: translateX(30px);
}
</style>

./src/components/Chat/MessageForm.vue

<template>
  <div class="inner-wrap">
    <v-text-field
      v-model="msg"
      label="chat"
      placeholder="보낼 메세지를 입력하세요."
      solo
      @keyup.13="submitMessageFunc"
    ></v-text-field>
  </div>
</template>

<script>
export default {
  name: 'MessageForm',
  data() {
    return {
      msg: '',
    };
  },
  methods: {
    submitMessageFunc() {
      if (this.msg.length === 0) return false;
      this.$emit('submitMessage', this.msg);
      this.msg = '';
      return true;
    },
  },
};
</script>

views폴더에 ChatRoom.vue을 만들어줍니다.

.src/views/ChatRoom.vue

<template>
  <div class="inner-wrap" fluid fill-height inner-wrap>
    <Message-List :msgs="msgDatas" class="msg-list"></Message-List>
    <Message-From v-on:submitMessage="sendMessage" class="msg-form" ></Message-From>
  </div>
</template>

<script>
import { mapMutations, mapState } from 'vuex';
import MessageList from '@/components/Chat/MessageList.vue';
import MessageForm from '@/components/Chat/MessageForm.vue';
import Constant from '@/Constant';

export default {
  name: 'ChatRoom',
  data() {
    return {
      datas: [],
    };
  },
  components: {
    'Message-List': MessageList,
    'Message-From': MessageForm,
  },
  computed: {
    ...mapState({
      'msgDatas': state => state.socket.msgDatas,
    }),
  },
  created() {
    const $ths = this;
    this.$socket.on('chat', (data) => {
      this.pushMsgData(data);
      $ths.datas.push(data);
    });
  },
  methods: {
    ...mapMutations({
      'pushMsgData': Constant.PUSH_MSG_DATA,
    }),
    sendMessage(msg) {
      this.pushMsgData({
        from: {
          name: '나',
        },
        msg,
      });
      this.$sendMessage({
        name: this.$route.params.username,
        msg,
      });
    },
  },
};
</script>

<style>
.msg-form {
  bottom: -28px;
  position: absolute;
  left: 0;
  right: 0;
}
.msg-list {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 60px;
  overflow-x: scroll;
}
</style>

라우터에 Path를 추가해줍니다.

./src/router.js

import Vue from 'vue';
import Router from 'vue-router';
import Login from './views/Login.vue';
import ChatRoom from './views/ChatRoom.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'Login',
      component: Login,
    },
    {
      path: '/char-room/:username',
      name: 'ChatRoom',
      component: ChatRoom,
    },
  ],
});

채팅시 자동 스크롤 이동을 위해
directive를 만들어 보겠습니다.

./plugin/directives.vue

module.exports = (Vue) => {
  // dom 업데이트시 스크롤을 최하단으로 이동시킵니다.
  Vue.directive('auto-bottom', {
    update: (el) => {
      setTimeout(() => {
        el.scrollTop = el.scrollHeight;
      }, 0);
    },
  });
};

main.js에 import해줍니다.

import Directives from './plugins/directives';
Vue.use(Directives);

3. 서버 작업

$ mkdir chat-server 폴더를 생성합니다.
$ yarn init
$ yarn add express
$ yarn add socket.io

모듈을 설치합니다.

chat-server/app.js

var app = require('express')();
var server = require('http').createServer(app);
var io = require('socket.io')(server,{
  pingTimeout: 1000,
});

app.all('/*', function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  next();
});

// localhost:3000서버에 접속하면 클라이언트로 메세지을 전송한다
app.get('/', function(req, res) {
  res.sendFile('Hellow Chating App Server');
});

io.on('connection', function(socket){

  // 클라이언트로부터의 메시지가 수신되면
  socket.on('chat', function(data) {
    console.log('Message from %s: %s', data.name, data.msg);

    var msg = {
      from: {
        name: data.name,
      },
      msg: data.msg
    };

    // 메시지를 전송한 클라이언트를 제외한 모든 클라이언트에게 메시지를 전송한다
    socket.broadcast.emit('chat', msg);
  });

  socket.on('disconnect', function() {
    console.log('user disconnected: ' + socket.name);
  });


});

server.listen(3000);

결과물

스크린샷 2018-10-25 오후 9.19.26.png

참고

소스 깃허브