본문으로 건너뛰기

Pusher 를 이용한 실시간 채팅 웹앱 만들기 #5 (w/ Laravel, Nuxt)

변찬혁실시간 채팅 앱 만들기 (Pusher)PusherLaravelNuxtVueWeb

Pusher 를 이용한 실시간 채팅 웹앱 만들기 #5 (w/ Laravel, Nuxt)

이번 글에서는 채팅 앱 프론트엔드 코드를 작성하고 Pusher, 만들어놓은 Laravel 백엔드와 각각 연결해서 가장 간단한 채팅 앱을 만들어보겠습니다.

프론트엔드 구현

Pusher 연결을 위한 패키지 설치

우리의 Nuxt 프로젝트에서 Pusher를 연결하여 채널을 구독하기 위해서 pusher-js 패키지와 laravel-echo 패키지를 설치합니다.

npm install --save laravel-echo pusher-js

정보

Pusher와 연결하려면 pusher-js만 설치하여 연결해도 되지만 laravel-echo 패키지는 Laravel에서 보내는 이벤트를 더 편하게 받을 수 있도록 pusher-js를 감싼 패키지입니다. laravel-echo의 자세한 내용은 Laravel 문서를 참고open in new window하세요. pusher-js만 이용하여 연결하는 방법은 Pusher 문서를 참고open in new window하세요.

ChatApp 컴포넌트 작성

chat 채널에 구독하고 ChatSent Laravel 이벤트를 수신하여 화면에 목록으로 보여주며, 다른 사용자들에게 메세지를 보낼 수 있는 ChatApp 컴포넌트를 작성하겠습니다.

Nuxt에서 컴포넌트는 components/ 디렉토리에 작성합니다. 이 디렉토리에 있는 .vue 파일들은 nuxt.config.js 파일의 components 옵션이 true 라면 자동으로 임포트됩니다. (기본값)

components/ChatApp.vue 파일을 만들고 컴포넌트를 작성합니다.

<template>
  <div class="rounded shadow-lg border p-4">
    <!-- 채팅 제목 라인 -->
    <h1 class="text-lg font-bold">채팅</h1>
    <!-- 구분선 -->
    <div class="bg-gray-200 my-2" style="height: 1px"></div>
    <!-- 대화 로그 -->
    <ol class="rounded border p-2 mb-2" style="min-height: 200px">
      <!-- 채팅 아이템 -->
      <li v-for="chat in chats" :key="chat.chatAt" class="flex p-1">
        <!-- 송신자 -->
        <h2 class="font-bold tracking-wide mr-2">{{ chat.sender }}</h2>
        <!-- 내용 -->
        <div class="tracking-tight">
          {{ chat.message }}
        </div>
      </li>
      <li v-if="sending" class="text-right text-sm p-1">보내는 중..</li>
    </ol>
    <div class="flex flex-wrap text-sm">
      <div class="w-full mb-1 sm:w-1/4 sm:mb-0 sm:pr-1">
        <input class="w-full rounded border p-1 focus:outline-none focus:ring-1 focus:ring-blue-500" type="text" ref="sender" placeholder="이름" />
      </div>
      <div class="w-3/4 sm:w-2/4 sm:px-1">
        <input class="w-full rounded border p-1 focus:outline-none focus:ring-1 focus:ring-blue-500" type="text" ref="message" placeholder="메세지" @keydown.enter="send" />
      </div>
      <div class="w-1/4 pl-1">
        <button class="w-full h-full rounded bg-blue-500 text-white font-bold" @click="send">전송</button>
      </div>
    </div>
  </div>
</template>

<script>
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

export default {
  data() {
    return {
      token: '',
      chats: [],
      echo: null,
      sending: false,
    };
  },
  methods: {
    async send() {
      if (!this.sending) {
        const data = {
          sender: this.$refs.sender.value,
          message: this.$refs.message.value,
        };
        if (!this.validate(data)) {
          alert('입력을 확인해주세요!');
          return;
        }

        this.sending = true;
        this.$refs.message.value = '';
        this.$refs.message.focus();

        await this.$axios.post('http://localhost:8000/chat/send', data);

        this.sending = false;
      }
    },
    validate(data) {
      if (data.sender === '') return false;
      if (data.message === '') return false;
      return true;
    },
    onChatSent(event) {
      this.chats.push(event);
    },
    connect() {
      if (!window.Pusher) window.Pusher = Pusher;
      if (!this.echo) {
        this.echo = new Echo({
          broadcaster: 'pusher',
          key: 'e6e27d54525dcd0b0de1',
          cluster: 'ap3',
        });
      }

      this.echo.channel('chat').listen('ChatSent', (e) => {
        this.onChatSent(e);
      });
    },
    disconnect() {
      this.echo.leaveChannel('chat');
    },
  },
  mounted() {
    this.connect();
    window.addEventListener('beforeunload', this.disconnect);

    this.$refs.sender.focus();
  },
};
</script>

ChatApp 컴포넌트가 마운트되면 laravel-echo 라이브러리를 사용해 chat 채널의 ChatSent 이벤트를 구독하도록 했습니다. ChatSent 이벤트를 받으면 chats 배열에 추가하고 이 배열을 채팅창의 느낌으로 화면에 보여줬습니다.

하단의 텍스트박스에 이름과 메세지를 입력하고 엔터를 치거나 전송 버튼을 누르면 send 메서드를 통해 입력을 검증한 뒤 이전 포스트에서 만들어놓은 /chat/send URL로 보내 브로드캐스팅하여 다른 구독자들에게 알리도록 했습니다.

index 페이지 작성

ChatApp 컴포넌트를 만들었으니, 이 컴포넌트를 보여줄 페이지가 필요합니다.

Nuxt에서 페이지 컴포넌트는 pages/ 디렉토리에 작성합니다. 해당 디렉토리에 존재하는 .vue 파일들은 자동으로 자신의 파일명으로 라우트(vue-router)를 생성합니다. 참고open in new window

우리가 만든 컴포넌트를 / Path 로 들어오면 바로 보이도록, pages/ 디렉토리에 index.vue 파일을 만들어서 ChatApp 컴포넌트를 적당히 렌더링하겠습니다.

<template>
  <div class="container mx-auto p-3">
    <ChatApp class="mx-auto" style="max-width: 500px" />
  </div>
</template>

<script>
export default {}
</script>

Nuxt 개발 서버 실행 및 테스트

간단하게 프론트엔드 코드를 모두 작성했습니다.

이제 Nuxt 개발 서버를 실행해 우리가 만든 프론트엔드 코드를 제공하는 Node 서버를 실행시켜 접속해봅시다.

npm run dev

위 명령을 실행하면 잠시 뒤 서버가 실행됩니다. 기본적으로 http://localhost:3000open in new window 에서 실행됩니다. 크롬으로 접속해봅시다.

우리의 첫 채팅 앱이 완성되었습니다!

여러개의 탭이나 창을 띄워 여러개의 페이지를 띄우고, 그 중 하나의 페이지에서 이름과 메세지를 입력하고 전송을 눌러보세요.

하나의 페이지에서 메세지를 전송하면, 다른 페이지들에 그 채팅이 전달됩니다! 성공적입니다.

그런데 지금 서버의 코드는 DB를 사용하지 않아서 채팅 기록들이 남지 않고 새로고침하면 사라집니다.

Laravel 서버 DB 연동

간단하게 채팅 기록을 저장하는 테이블을 만들어 채팅들을 저장하고, 프론트에서는 채팅 앱이 처음 로딩될 때 서버에 요청하여 DB에 있는 기존 채팅들을 가져와 목록에 추가시켜보겠습니다.

Chat 모델 및 마이그레이션 생성

Laravel은 데이터베이스를 다룰 때, 일반적으로 SQL을 직접 사용하지 않고, Eloquent ORM을 사용하여 테이블을 모델로 추상화하여 다룰 수 있는 인터페이스를 제공합니다.

Laravel의 ORM 기능을 사용하기 위해 우리의 채팅 기록을 표현할 Chat 모델을 만들겠습니다. 모델이 만들어지는 폴더는 Laravel 8.x 기준 /app/Models 디렉토리입니다.

php artisan make:model Chat -m

모델을 만들면서 -m 옵션을 붙였습니다. -m 옵션을 붙이면 모델에 대한 마이그레이션 파일도 같이 생성합니다.

마이그레이션은 데이터베이스 스키마 조작을 프로그래밍적으로 정의해두고 실행할 수 있도록 해주는 Laravel의 기능입니다. 다른 환경 간에 쉽게 스키마를 동기화할 수 있도록 사용합니다. 참고open in new window

만들어진 마이그레이션 파일에 우리의 채팅 기록 데이터에 필요한 Column들을 추가하여 chats 테이블에 대한 마이그레이션을 작성합니다.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateChatsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('chats', function (Blueprint $table) {
            $table->id();
            $table->string('sender');
            $table->text('message');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('chats');
    }
}

보낸 사람의 이름을 저장하는 sender, 보낸 메세지를 저장하는 message Column을 추가했습니다.

마이그레이션을 모두 작성했으면, 아래 명령을 실행해 추가한 마이그레이션을 실행합니다. 해당 마이그레이션이 실행되면 chats 테이블이 생성됩니다.

php artisan migrate

이제 Chat 모델 파일을 보겠습니다.

Laravel의 모델은 기본적으로 모델 이름을 복수형으로 바꾼 것의 snakeCase을 이름으로 가진 테이블을 찾아 연결됩니다. 모델을 만들때 -m 옵션을 붙여 생성하면 생성되는 마이그레이션의 테이블 이름은 기본적으로 이 형태를 띕니다. 따라서 모델이 어느 테이블에 대한 건지 테이블 이름을 지정해 줄 필요가 없습니다.

나중에 대량 할당(Mass Assignments)를 사용하기 위해 guarded 속성을 []로 설정하여 모든 프로퍼티를 대량 할당 가능하게 합니다. 참고open in new window

모델 준비는 이것으로 끝났습니다. 이제 ChatController를 수정하면서 Chat 모델을 사용하여 DB와 연동해보겠습니다.

ChatController 개선

기존 send 메서드에 validation을 추가하고, Chat 모델을 이용해 데이터베이스에 새 채팅 데이터를 넣은 후 브로드캐스팅 하도록 수정합니다.

protected $rules = [
    'sender' => 'required',
    'message' => 'required'
];

public function send()
{
    $validated = request()->validate($this->rules);

    $chat = Chat::create($validated);

    broadcast(new ChatSent($chat->sender, $chat->message, now()));

    return response([
        'result' => 'success',
        'chat' => $chat
    ]);
}

프론트에서 ChatApp이 처음 로드될 때, 서버 데이터베이스에 담겨있는 기존 대화 기록들을 가져올 수 있어야 합니다. 이를 위한 load 메서드를 만듭니다.

public function load()
{
    $chats = Chat::orderBy('created_at')->get();

    return response([
        'result' => 'success',
        'chats' => $chats
    ]);
}

라우트 추가

새로 만든 load 메서드에 대한 라우트를 만들어줍니다. [GET] /chats 요청을 연결하겠습니다.

Route::get('/chats', [ChatController::class, 'load']);

백엔드 쪽 준비는 끝났습니다.

ChatApp 컴포넌트 개선

<template>
  <!-- ... -->
</template>

<script>
// ...

export default {
  data() {
    return {
      token: '',
      chats: [],
      echo: null,
      sending: false,
    };
  },
  // ...
  async fetch() {
    const { data } = await this.$axios.get('http://localhost:8000/chats');
    this.chats = data.chats;
  },
  // ...
};
</script>

기존에서 추가한 부분은 fetch 메서드 하나입니다.

fetch 메서드를 추가하여 그 내부에서 [GET] /chats 요청을 하고 그것을 채팅 목록에 초기화합니다. fetch 메서드는 Nuxt에서 제공해주는 lifecycle hook 입니다. SSR 모드에서, 페이지를 렌더링 하기 전에 API 호출 등을 통해 데이터를 가져올 수 있게 합니다.

완료

완성입니다. Laravel 프로젝트를 php artisan serve 명령으로 띄우고, Nuxt 프로젝트에서 npm run dev 명령을 실행하고 http://localhost:3000open in new window 에 들어가서 채팅을 몇번 쳐보고 새로고침을 눌러보세요.

이전과는 다르게 페이지가 다시 로딩되어도 기존 채팅 기록이 사라지지 않는 것을 확인할 수 있습니다.

마지막 수정 일시:
기여자: Chanhyuk Byeon