PWA

사용자 인증 - 계정 만들기

그랜파 개발자 2024. 8. 3. 03:46

43. 계정 만들기

1. 계정 만들기 시퀀스

계정 만들기는 다음과 같은 시퀀스를 가집니다.

  1. 사용자는 이메일, 이름, 비밀번호, 비밀번호 확인을 입력 후 ‘Register’를 누릅니다. 비밀 번호 확인 입력 중 비밀번호와 비밀번호 확인이 진행됩니다.
  2. my-auth 웹앱은 이메일과 비밀번호로 구글에 계정 만들기를 진행합니다.
  3. 구글 계정 만들기를 위하여 createUserWithEmailAndPassword() 함수를 사용합니다.
  4. 구글에 계정 만들기를 성공하면 구글은 user 객체를 돌려줍니다.
  5. 사용자가 입력한 이메일, 이름, user객체의 uid로 웹앱의 firestore DB의 계정 컬렉션에 계정 정보를 저장합니다.

2. UI

개발에 필요한 몇 가지 질문

ChatGPT에게 물었습니다.

1. createUserWithEmailAndPassword(email, password)의 모든 에러 메시지를 알려줘

 

createUserWithEmailAndPassword(email, password) 메서드와 관련된 모든 에러 메시지는 Firebase 인증 문서에서 확인할 수 있습니다. 아래는 이 메서드와 관련된 모든 에러 코드와 그 설명입니다.

모든 에러 메시지 목록

  1. auth/email-already-in-use 설명: 이미 사용 중인 이메일 주소로 계정을 생성하려고 할 때 발생합니다. 예시: "The email address is already in use by another account."
  2. auth/invalid-email 설명: 유효하지 않은 이메일 주소 형식일 때 발생합니다. 예시: "The email address is badly formatted."
  3. auth/operation-not-allowed 설명: 이메일/비밀번호 계정 생성이 비활성화되어 있을 때 발생합니다. 예시: "Password sign-in is disabled for this project."
  4. auth/weak-password 설명: 비밀번호가 너무 약할 때 발생합니다. Firebase에서는 최소 6자 이상의 비밀번호를 요구합니다. 예시: "The password must be 6 characters long or more."
  5. auth/invalid-password 설명: 제공된 비밀번호가 유효하지 않을 때 발생합니다. 예시: "The password is invalid or the user does not have a password."
  6. auth/user-disabled 설명: 해당 사용자가 비활성화되어 있을 때 발생합니다. 예시: "The user account has been disabled by an administrator."
  7. auth/user-not-found 설명: 이메일이 등록되지 않은 경우 발생합니다. 예시: "There is no user record corresponding to this identifier. The user may have been deleted."
  8. auth/too-many-requests 설명: 비정상적인 요청으로 인해 요청이 차단되었을 때 발생합니다. 예시: "We have blocked all requests from this device due to unusual activity. Try again later."
  9. auth/network-request-failed 설명: 네트워크 오류로 인해 요청이 실패했을 때 발생합니다. 예시: "A network error (such as timeout, interrupted connection or unreachable host) has occurred."

예제 코드

모든 에러 메시지를 처리하는 예제 코드는 다음과 같습니다.

import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";

const auth = getAuth();

const registerUser = async (email, password) => {
  try {
    const userCredential = await createUserWithEmailAndPassword(auth, email, password);
    const user = userCredential.user;
    console.log("User created:", user);
  } catch (error) {
    const errorCode = error.code;
    const errorMessage = error.message;

    switch (errorCode) {
      case 'auth/email-already-in-use':
        console.error("Email already in use:", errorMessage);
        break;

      case 'auth/invalid-email':
        console.error("Invalid email:", errorMessage);
        break;

      case 'auth/operation-not-allowed':
        console.error("Operation not allowed:", errorMessage);
        break;

      case 'auth/weak-password':
        console.error("Weak password:", errorMessage);
        break;

      case 'auth/invalid-password':
        console.error("Invalid password:", errorMessage);
        break;

      case 'auth/user-disabled':
        console.error("User disabled:", errorMessage);
        break;

      case 'auth/user-not-found':
        console.error("User not found:", errorMessage);
        break;

      case 'auth/too-many-requests':
        console.error("Too many requests:", errorMessage);
        break;

      case 'auth/network-request-failed':
        console.error("Network request failed:", errorMessage);
        break;

      default:
        console.error("Unknown error:", errorMessage);
    }
  }
};

// 예제 사용
registerUser('test@example.com', 'password123');

 

위 예제는 Firebase Authentication에서 발생할 수 있는 다양한 에러 메시지를 처리하는 방법을 보여줍니다. Firebase 문서를 참고하여 더 자세한 정보를 얻을 수 있습니다.

2. v-text-field type의 종류는?

Vuetify의 v-text-field 컴포넌트에서 사용할 수 있는 type 속성의 종류는 HTML5 표준 입력 타입과 동일합니다. 이를 통해 다양한 유형의 입력 필드를 생성할 수 있습니다. 다음은 v-text-field에서 지원하는 주요 type 속성의 목록입니다:

  1. text - 일반 텍스트 입력 필드 (기본값)
  2. password - 비밀번호 입력 필드
  3. email - 이메일 주소 입력 필드
  4. number - 숫자 입력 필드
  5. tel - 전화번호 입력 필드
  6. url - URL 입력 필드
  7. search - 검색 입력 필드
  8. date - 날짜 선택 필드
  9. time - 시간 선택 필드
  10. datetime-local - 로컬 날짜 및 시간 선택 필드
  11. month - 월 선택 필드
  12. week - 주 선택 필드
  13. color - 색상 선택 필드
  14. file - 파일 선택 필드

예제 코드

다양한 type 속성을 사용하는 v-text-field의 예제 코드는 다음과 같습니다:

<template>
  <v-container>
    <!-- Text Field -->
    <v-text-field label="Text" type="text" ></v-text-field>

    <!-- Password Field -->
    <v-text-field label="Password" type="password"></v-text-field>

    <!-- Email Field -->
    <v-text-field label="Email" type="email"></v-text-field>

    <!-- Number Field -->
    <v-text-field label="Number" type="number"></v-text-field>

    <!-- Tel Field -->
    <v-text-field label="Phone" type="tel"></v-text-field>

    <!-- URL Field -->
    <v-text-field label="URL" type="url"></v-text-field>

    <!-- Search Field -->
    <v-text-field label="Search" type="search"></v-text-field>

    <!-- Date Field -->
    <v-text-field label="Date" type="date"></v-text-field>

    <!-- Time Field -->
    <v-text-field label="Time" type="time"></v-text-field>

    <!-- Datetime-local Field -->
    <v-text-field label="Datetime" type="datetime-local"></v-text-field>

    <!-- Month Field -->
    <v-text-field label="Month" type="month"></v-text-field>

    <!-- Week Field -->
    <v-text-field label="Week" type="week"></v-text-field>

    <!-- Color Field -->
    <v-text-field label="Color" type="color"></v-text-field>

    <!-- File Field -->
    <v-text-field label="File" type="file"></v-text-field>
</template>

<script>
export default {
  name: 'TextFieldTypesExample'
}
</script>

 

이 예제에서는 다양한 입력 타입을 사용하는 v-text-field 컴포넌트를 보여줍니다. 이를 통해 사용자는 원하는 입력 타입을 선택하여 사용할 수 있습니다.

3. store의 actions 함수에서 actions 함수를 호출하는 법은? 그리고 매개변수를 전달하고 싶다면?

Vuex의 actions 함수에서 다른 actions 함수를 호출할 때 매개변수를 전달하는 방법은 dispatch 메서드의 두 번째 인수로 전달하는 것입니다. 이 두 번째 인수는 액션 함수에 전달될 payload입니다.

예제

다음은 store의 actions 함수에서 다른 actions 함수를 호출하면서 매개변수를 전달하는 예제입니다.

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload.amount;
    }
  },
  actions: {
    increment({ commit }, payload) {
      commit('increment', payload);
    },
    incrementMultipleTimes({ dispatch }, payload) {
      for (let i = 0; i < payload.times; i++) {
        dispatch('increment', { amount: payload.amount });
      }
    }
  }
});

 

위 예제에서 incrementMultipleTimes 액션은 increment 액션을 호출하면서 매개변수를 전달하여 상태의 count 값을 여러 번 증가시킵니다.

전체 Vue 컴포넌트 예제

아래는 Vue 컴포넌트와 함께 Vuex 스토어를 사용하는 예제입니다.

 

store.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload.amount;
    }
  },
  actions: {
    increment({ commit }, payload) {
      commit('increment', payload);
    },
    incrementMultipleTimes({ dispatch }, payload) {
      for (let i = 0; i < payload.times; i++) {
        dispatch('increment', { amount: payload.amount });
      }
    }
  }
});

 

App.vue

<!-- App.vue -->
<template>
  <v-app>
    <v-container>
      <div>
        <p>Count: {{ count }}</p>
        <v-btn @click="increment({ amount: 1 })">Increment</v-btn>
        <v-btn @click="incrementMultipleTimes({ amount: 1, times: 3 })">Increment Three Times</v-btn>
      </div>
    </v-container>
  </v-app>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapActions(['increment', 'incrementMultipleTimes'])
  }
}
</script>

<style>
/* Add your styles here */
</style>

설명

store.js:

  • state 객체에는 count 상태 변수가 있습니다.
  • mutations 객체에는 increment 변이가 있습니다. 이 변이는 amount를 인자로 받아 count 상태를 증가시킵니다.
  • actions 객체에는 increment와 incrementMultipleTimes 액션이 있습니다.
  • increment 액션은 commit을 통해 increment 변이를 호출하며 payload를 전달합니다.
  • incrementMultipleTimes 액션은 dispatch를 사용하여 increment 액션을 여러 번 호출하며 payload를 전달합니다.

App.vue:

  • mapState와 mapActions 헬퍼를 사용하여 상태와 액션을 매핑합니다.
  • 버튼 클릭 시 increment와 incrementMultipleTimes 액션을 호출하여 count 상태를 증가시킵니다. 이때 매개변수를 전달합니다.

이 예제를 통해 Vuex 스토어의 actions 함수에서 다른 actions 함수를 호출할 때 매개변수를 전달하는 방법을 확인할 수 있습니다.

4. store에서 router을 사용하는 법은?

Vuex store에서 Vue Router를 사용하는 방법은 Vue Router 인스턴스를 직접 store로 가져와서 사용하는 것입니다. 이를 통해 Vuex 액션이나 뮤테이션 내에서 라우팅 작업을 수행할 수 있습니다. 예를 들어, 특정 액션이 완료된 후에 다른 페이지로 이동해야 하는 경우에 유용합니다.

다음은 Vuex store에서 Vue Router를 사용하는 방법을 보여주는 예제입니다.

1. Vue Router 설정

먼저 Vue Router를 설정합니다.

// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import About from './views/About.vue';

Vue.use(Router);

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: About
    }
  ]
});

export default router;

2. Vuex Store 설정

Vuex Store에서 Vue Router 인스턴스를 사용하려면 이를 import하여 사용합니다.

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import router from './router';  // Vue Router import

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment({ commit }) {
      commit('increment');
    },
    incrementAndNavigate({ commit }) {
      commit('increment');
      router.push('/about');  // 라우터를 사용하여 페이지 이동
    }
  }
});

3. Vue 컴포넌트 설정

Vue 컴포넌트에서 Vuex 액션을 호출하여 라우팅 작업을 수행합니다.

<!-- App.vue -->
<template>
  <v-app>
    <v-container>
      <div>
        <p>Count: {{ count }}</p>
        <v-btn @click="increment">Increment</v-btn>
        <v-btn @click="incrementAndNavigate">Increment and Navigate to About</v-btn>
      </div>
    </v-container>
  </v-app>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapActions(['increment', 'incrementAndNavigate'])
  }
}
</script>

<style>
/* Add your styles here */
</style>

설명

router.js: - Vue Router 설정 파일입니다. 홈 페이지와 어바웃 페이지로의 라우팅을 정의합니다.

store.js: - Vuex Store 파일입니다. Vue Router 인스턴스를 import하고 incrementAndNavigate 액션에서 라우터를 사용하여 /about 페이지로 이동합니다.

App.vue: - Vue 컴포넌트 파일입니다. 버튼 클릭 시 increment와 incrementAndNavigate 액션을 호출하여 상태를 변경하고 라우팅 작업을 수행합니다.

이 예제를 통해 Vuex Store에서 Vue Router를 사용하는 방법을 확인할 수 있습니다. 이를 통해 상태 관리와 라우팅 작업을 함께 처리할 수 있습니다.

5. app-bar에서 타이틀은 왼쪽, 로그아웃 아이콘은 오른쪽에 두고 싶어.

Vuetify의 v-app-bar 컴포넌트를 사용하여 타이틀을 왼쪽에 두고, 로그아웃 아이콘을 오른쪽에 두는 방법을 보여드리겠습니다. Vuetify의 v-spacer 컴포넌트를 사용하면 요소들을 쉽게 정렬할 수 있습니다.

다음은 예제 코드입니다:

<template>
  <v-app>
    <v-app-bar app>
      <v-toolbar-title>My App</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn icon @click="logout">
        <v-icon>mdi-logout</v-icon>
      </v-btn>
    </v-app-bar>
    <v-main>
      <!-- Your content here -->
    </v-main>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  methods: {
    logout() {
      // 로그아웃 로직을 여기에 작성
      console.log('Logged out');
    }
  }
}
</script>

<style>
/* Add your styles here */
</style>

설명

  • v-app-bar: 앱의 상단 바를 나타냅니다.
  • v-toolbar-title: 타이틀을 표시하는 컴포넌트입니다. 타이틀은 기본적으로 왼쪽에 정렬됩니다.
  • v-spacer: 가능한 모든 공간을 차지하여 그 사이에 있는 요소들을 양쪽 끝으로 밀어줍니다.
  • v-btn: 로그아웃 아이콘을 클릭 가능한 버튼으로 만듭니다.
  • v-icon: 아이콘을 표시합니다. 여기서는 mdi-logout 아이콘을 사용합니다.
  • logout 메서드: 로그아웃 버튼이 클릭되었을 때 실행되는 메서드입니다. 실제 로그아웃 로직은 이 메서드 안에 작성해야 합니다.

이 예제를 통해 타이틀을 왼쪽에 두고, 로그아웃 아이콘을 오른쪽에 배치하는 방법을 확인할 수 있습니다. v-spacer 컴포넌트가 중앙의 빈 공간을 차지하여 자연스럽게 요소들을 양 끝으로 배치해줍니다.

계정 만들기

src/views/Register.vue 

Source

src/views/Register.vue

<!-- src/views/Register.vue -->
<template>
  <v-container>
    <v-row>
      <v-col cols="12" class="text-center my-5">
        <h3>계정 만들기</h3>
      </v-col>      
    </v-row>
    <v-row>
      <v-col class="text-center" cols="10" offset="1" sm="8" offset-sm="2">    
        <v-form @submit.prevent="userRegister">
          <v-text-field v-model="email" label="이메일" type="email" required></v-text-field>
          <v-text-field v-model="name" label="이름" type="text" required></v-text-field>
          <v-text-field v-model="password" label="비밀번호" type="password" required></v-text-field>
          <v-text-field v-model="confirmPassword" name="confirmPassword" label="비밀번호 확인" type="password" required :rules="[comparePassword]"></v-text-field>
          <v-progress-circular v-if="isLoading" indeterminate :width="7" :size="70" color="grey lighten-1"></v-progress-circular>    
          <v-btn type="submit" color="primary">Register</v-btn>
          <v-alert v-if="error" type="error" dismissible>{{ error }}</v-alert>
        </v-form>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';

export default {
  data() {
    return {
      email: '',
      name: '',
      password: '',
      confirmPassword: ''
    };
  },
  computed: {
    ...mapGetters(['error', 'isLoading']),
    comparePassword() {
      if(this.password == this.confirmPassword) return true;
      else return "비밀번호가 일치하지 않습니다.";
    },
  },
  methods: {
    ...mapActions(['register']),
    async userRegister() {
      await this.register({ email: this.email, password: this.password, name: this.name });
    }
  }
};
</script>

 

src/store/index.js

Copy// src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import { db, auth, googleProvider } from '@/firebase';
import router from '@/router';  // Vue Router import

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    user: null,
    users: [],
    isLoading: false,
    error: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user;
    },
    setUsers(state, users) {
      state.users = users;
      state.isLoading = false;
    },
    setError(state, error) {
      state.error = error;
      state.isLoading = false;
    },
    setLoading(state, isLoading) {
      state.isLoading = isLoading;
    }
  },
  actions: {
    async fetchUsers({ commit }) {
      commit('setLoading', true);
      try {
        const usersSnapshot = await db.collection('users').get();
        const users = usersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
        commit('setUsers', users);
      } catch (error) {
        commit('setError', error.message);
      }
    },
    async addUser({ dispatch }, user) {
      try {
        await db.collection('users').add(user);
        dispatch('fetchUsers');
      } catch (error) {
        console.error('Error adding user:', error);
      }
    },
    async deleteUser({ dispatch }, userId) {
      try {
        await db.collection('users').doc(userId).delete();
        dispatch('fetchUsers');
      } catch (error) {
        console.error('Error deleting user:', error);
      }
    },
    async updateUser({ dispatch }, { userId, user }) {
      try {
        await db.collection('users').doc(userId).update(user);
        dispatch('fetchUsers');
      } catch (error) {
        console.error('Error updating user:', error);
      }
    },
    async login({ commit }, { email, password }) {
      try {
        const { user } = await auth.signInWithEmailAndPassword(email, password);        
        commit('setUser', user);
        router.push("/");   // home으로
      } catch (error) {
        commit('setError', error.message);
      }
    },
    async register({ commit, dispatch }, { email, password, name }) {
      try {
        const { user } = await auth.createUserWithEmailAndPassword(email, password, name);

        // 웹앱에 계정을 만든다.
        const newUser = {
          email: email,
          name: name,
          uids: [user.uid]
        };
        dispatch('addUser', newUser);

        // 로그인 설정
        commit('setUser', newUser);
        //commit('setUser', user);

        router.push("/");   // home으로
      } catch (error) {
        console.log(error);
        commit('setError', error.message);
      }
    },
    async googleLogin({ commit }) {
      try {
        const { user } = await auth.signInWithPopup(googleProvider);
        commit('setUser', user);
      } catch (error) {
        commit('setError', error.message);
      }
    },
    async logout({ commit }) {
      await auth.signOut();
      commit('setUser', null);
    },
    setUser({ commit }, user) {
      commit('setUser', user);
    }
  },
  getters: {
    user: state => state.user,
    users: state => state.users,
    isLoading: state => state.isLoading,
    error: state => state.error
  }
});

'PWA' 카테고리의 다른 글

사용자 인증 - 자동 로그인  (0) 2024.08.06
사용자 인증 - 로그인  (0) 2024.08.05
사용자 인증 - 프로젝트  (0) 2024.08.03
사용자 인증 - UI : Vuetify Component  (0) 2024.08.01
사용자 인증 - 개요, Router  (0) 2024.08.01