개발 도구 (Development Tools)/Vue

#6. Vue 3 실전 예제

BiCute 2023. 6. 5. 08:00
반응형

 

# 간단한 CRUD 앱 만들기

 

우선 CRUD는 생성(Create), 읽기(Read), 수정(Update), 삭제(Delete)의 약자로 데이터베이스나 서버에서 데이터를 다룰 때 가장 많이 사용하는 기능입니다.

이번 예제에서는 간단한 할 일 목록을 다루는 CRUD 기능을 구현해 볼 것입니다

(1) 먼저 src/components 폴더 안에 TodoList.vue 컴포넌트를 만듭니다.

<template>
    <div>
        <h2>할 일 목록</h2>
        <ul>
            <li v-for="(todo, index) in todos" :key="index">
                {{ todo }}
                <button @click="deleteTodo(index)">삭제</button>
                <button @click="editTodo(index)">수정</button>
            </li>
        </ul>
        <div>
            <input type="text" v-model="newTodo" />
            <button v-if="!editing" @click="addTodo">추가</button>
            <button v-if="editing" @click="cancelEdit">취소</button>
            <button v-if="editing" @click="updateTodo">수정</button>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            todos: ["Vue.js 배우기", "할 일 목록 만들기", "CRUD 기능 추가하기"],
            newTodo: "",
            editing: false,
            editedIndex: null,
        };
    },
    methods: {
        addTodo() {
            if (this.newTodo) {
                if (this.editing) {
                    this.updateTodo();
                } else {
                    this.todos.push(this.newTodo);
                    this.newTodo = "";
                }
                this.editing = false;
                this.editedIndex = null;
            }
        },
        deleteTodo(index) {
            this.todos.splice(index, 1);
        },
        editTodo(index) {
            this.newTodo = this.todos[index];
            this.editing = true;
            this.editedIndex = index;
        },
        updateTodo() {
            if (this.newTodo && this.editedIndex !== null) {
                this.todos.splice(this.editedIndex, 1, this.newTodo);
                this.newTodo = "";
                this.editing = false;
                this.editedIndex = null;
            }
        },
        cancelEdit() {
            this.newTodo = "";
            this.editing = false;
            this.editedIndex = null;
        },
    },
};
</script>

이 컴포넌트는 할 일 목록을 표시하고, 새로운 할 일을 추가하거나 삭제하는 기능을 가지고 있습니다. data 속성에서 todos 배열에 기본 할 일 목록을 설정하고, newTodo 변수에 새로 추가할 할 일을 저장합니다. methods에서는 addTodo 함수를 통해 새로운 할 일을 todos 배열에 추가하고, deleteTodo 함수를 통해 선택한 할 일을 삭제합니다.

 

(2) 이제 src/views 폴더 안에 HomeView.vue 컴포넌트를 만듭니다.

<template>
  <div>
    <TodoList />
  </div>
</template>

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

export default {
  name: 'HomeView',
  components: {
    TodoList,
  },
};
</script>

 

이 컴포넌트는 TodoList.vue 컴포넌트를 불러와서 HomeView 컴포넌트 안에 표시합니다.

 


(3) 마지막으로 src/router/index.js 파일을 열고, 다음과 같이 코드를 수정합니다.

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
  ]
})

export default router

이 코드는 라우터 모듈을 초기화하고, 라우트를 정의합니다. / 경로에서 HomeView 컴포넌트를 렌더링하도록 설정합니다.

 

 

 

# 인증 기능 추가하기

 

Vue.js에서 인증(Authentication) 기능을 구현하기 위해서는 일반적으로 토큰 기반 인증 방식을 사용합니다. 이 방식은 사용자가 로그인하면 서버에서 토큰을 발급하고, 이후 요청에서는 발급된 토큰을 사용하여 사용자를 인증하는 방식입니다.

이를 구현하기 위해서는 먼저 로그인 폼을 구현하고, 로그인 요청을 처리하는 API를 작성해야 합니다. 그리고 로그인 요청이 성공하면, 서버에서 발급한 토큰을 클라이언트에 저장하고, 이후 요청에서는 토큰을 포함하여 보내야 합니다. 또한, 토큰이 만료된 경우에는 새로운 토큰을 발급받아야 합니다.

다음은 Vue.js에서 인증 기능을 구현하는 예제입니다.
실제로 서버와 통신을 하는것이 아니기에 사용자 정보를 하드코딩하여 인증 기능을 구현하고, 사용자 정보가 배열에서 확인되면 로그인이 가능한 형식으로 작성되어있습니다.

<template>
  <div>
    <h2>로그인</h2>
    <form @submit.prevent="login">
      <div>
        <label>아이디</label>
        <input type="text" v-model="username">
      </div>
      <div>
        <label>비밀번호</label>
        <input type="password" v-model="password">
      </div>
      <button type="submit">로그인</button>
    </form>
    <div v-if="error">{{ error }}</div>
    <div v-else-if="token">토큰: {{ token }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: '',
      token: null,
      error: null,
    };
  },
  methods: {
    async login() {
      try {
        // 사용자 정보
        const users = [
          { id: 1, username: 'admin', password: 'admin1234' },
          { id: 2, username: 'user1', password: 'password1' },
          { id: 3, username: 'user2', password: 'password2' },
        ];

        // 사용자 정보 확인
        const user = users.find(u => u.username === this.username && u.password === this.password);
        if (user) {
          // 토큰 발급
          const token = '1234567890';
          this.token = token;
          this.error = null;
          // 토큰을 localStorage에 저장
          localStorage.setItem('token', token);
        } else {
          this.token = null;
          this.error = '아이디 또는 비밀번호가 일치하지 않습니다.';
          localStorage.removeItem('token');
        }
      } catch (error) {
        this.error = error.message;
      }
    },
  },
  mounted() {
    // 페이지가 로드될 때 localStorage에서 토큰을 확인
    const token = localStorage.getItem('token');
    if (token) {
      this.token = token;
    }
  },
};
</script>

위 코드에서는 사용자 정보를 하드코딩하여 users 배열에 저장합니다. 로그인 요청이 들어오면, 배열에서 사용자 정보를 확인하고, 사용자 이름과 비밀번호가 일치하는 경우, 하드코딩된 토큰을 발급하고, 발급된 토큰을 클라이언트에 저장합니다. 이후 페이지가 로드될 때, 저장된 토큰을 확인하고, 토큰이 있을 경우 화면에 토큰을 출력합니다.

실제 애플리케이션에서는 이 부분을 서버에서 처리해야 하며, 데이터베이스와 연동하여 실제 사용자 정보를 확인해야 합니다.

token 또한 하드코딩이 아닌, this.token = data.token 처럼 서버에서 암호화되어 받아온 값을 지정해 줍니다.

 

 

 

# 실시간 채팅 앱 만들기

 

실시간 채팅을 구현하는 경우에는 일반적으로 서버를 사용하여 구현하는 것이 보다 안정적이고 확장성이 높습니다.

가볍게 만드는 만큼 임시로 localStorage를 활용하여, 서버를 사용하지 않고 채팅을 구현하는식으로 작성해 보았습니다.

다음은 localStorage를 활용하여 실시간 채팅 애플리케이션을 구현한 예제입니다.

<template>
    <div class="chat-container">
        <div class="chat" ref="chat1">
            <div v-if="!username1">
                <h2>사용자1의 이름을 입력하세요</h2>
                <input
                    type="text"
                    v-model="tempUser1"
                    v-on:keyup.enter="updateUser1"
                    placeholder="이름 입력"
                    :disabled="username1" />
                <button @click="updateUser1" :disabled="username1">확인</button>
            </div>
            <div v-else>
                <h2>{{ username1 }}</h2>
                <div v-for="(message, index) in messages" :key="index">
                    <div
                        :class="{
                            'other-message': message.username !== username1,
                            'my-message': message.username === username1,
                        }">
                        <strong>{{ message.username }}:</strong>
                        {{ message.text }}
                    </div>
                </div>
                <form @submit.prevent="sendMessage(username1)">
                    <input
                        type="text"
                        v-model="text1"
                        placeholder="메시지를 입력하세요" />
                    <button type="submit">보내기</button>
                </form>
            </div>
        </div>
        <div class="chat" ref="chat2">
            <div v-if="!username2">
                <h2>사용자2의 이름을 입력하세요</h2>
                <input
                    type="text"
                    v-model="tempUser2"
                    v-on:keyup.enter="updateUser2"
                    placeholder="이름 입력"
                    :disabled="username2" />
                <button @click="updateUser2" :disabled="username2">확인</button>
            </div>
            <div v-else>
                <h2>{{ username2 }}</h2>
                <div v-for="(message, index) in messages" :key="index">
                    <div
                        :class="{
                            'other-message': message.username !== username2,
                            'my-message': message.username === username2,
                        }">
                        <strong>{{ message.username }}:</strong>
                        {{ message.text }}
                    </div>
                </div>
                <form @submit.prevent="sendMessage(username2)">
                    <input
                        type="text"
                        v-model="text2"
                        placeholder="메시지를 입력하세요" />
                    <button type="submit">보내기</button>
                </form>
            </div>
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            username1: null,
            text1: "",
            username2: null,
            text2: "",
            messages: [],
        };
    },
    methods: {
        updateUser1() {
            this.username1 = this.tempUser1;
        },
        updateUser2() {
            this.username2 = this.tempUser2;
        },
        sendMessage(username) {
            const message = {
                username,
                text: username === this.username1 ? this.text1 : this.text2,
            };
            this.messages.push(message);
            localStorage.setItem("messages", JSON.stringify(this.messages));
            if (username === this.username1) {
                this.text1 = "";
            } else {
                this.text2 = "";
            }
        },
        handleStorage(e) {
            if (e.key === "messages") {
                this.messages = JSON.parse(e.newValue) || [];
            }
        },
    },
    mounted() {
        window.addEventListener("storage", this.handleStorage);
        this.messages = JSON.parse(localStorage.getItem("messages")) || [];
    },
    unmounted() {
        window.removeEventListener("storage", this.handleStorage);
    },
};
</script>
<style scoped>
.chat-container {
    display: flex;
    justify-content: space-between;
    width: 800px;
    margin: 0 auto;
}

.chat {
    width: 45%;
    height: 500px;
    border: 1px solid #ccc;
    padding: 10px;
    overflow-y: scroll;
}

.other-message {
    margin-bottom: 10px;
    padding: 10px;
    background-color: #eee;
}

.my-message {
    margin-bottom: 10px;
    padding: 10px;
    background-color: #e6f2ff;
    text-align: right;
}
</style>

 

HTML 템플릿에서는 채팅방을 나타내는 두 개의 div를 정의합니다. v-if 지시자를 사용하여 사용자 이름이 없는 경우 이름을 입력 받는 폼을 표시하고, 사용자 이름이 있는 경우 채팅을 표시합니다. 이름을 입력 받을 때는 v-on:keyup.enter 지시자를 사용하여 Enter 키를 누르면 updateUser1 또는 updateUser2 메서드가 호출됩니다. 사용자 이름을 업데이트하면 username1 또는 username2 데이터가 갱신됩니다.

sendMessage 메서드는 사용자 이름을 매개변수로 받으며, 메시지를 messages 데이터 배열에 추가합니다. localStorage에 messages 데이터를 저장합니다. 사용자 이름이 username1이면 text1을, username2이면 text2를 사용하여 메시지를 가져옵니다. 마지막으로 사용자 이름이 username1이면 text1을, username2이면 text2를 비웁니다.

handleStorage 메서드는 localStorage에서 messages 데이터를 가져와 messages 배열을 갱신합니다.

이벤트 리스너를 등록하여 localStorage가 갱신될 때마다 handleStorage 메서드가 호출되도록 합니다.

Vue.js 컴포넌트의 라이프사이클 훅을 사용하여 이벤트 리스너를 등록 및 해제합니다.

마운트시 mounted()에서 localStorage에서 messages 데이터를 가져옵니다.

CSS 스타일 시트는 채팅방의 레이아웃과 메시지 스타일을 정의합니다.

각 메시지에 대해 other-message 또는 my-message 클래스를 추가하여 메시지 스타일을 구분합니다.



반응형