Vue3로 Firabase 로그인

개시하다


이 글은 2년 전에 쓴 것Vue vuex에서 Firebase 로그인 유지으로 Vue3+Composition API로 다시 썼다.구조도 바뀌었다.

자습서


시위 행진 여기 있다https://vue-firebase-demo-ebdef.web.app/
GiitHub에 소스 코드 업로드ErgoFriend/vue-firebase-auth
Image from Gyazo
다음과 같은 요소를 결합하여 여러 개의 구성 요소가 상태를 공유하는 Vue 응용 프로그램을 제작한다.
  • TypeScript
  • SPA
  • Vue3
  • Composition API
  • 스토어 모델
  • vue-router 기반 변환
  • Firebase Authentication
  • 절차는 다음과 같다.
  • 환경 구축
  • 제작 외관(UI)
  • 상태 관리 추가(store)
  • Firebase 도입(로그인/로그아웃)
  • 각 파일의 최종 코드 블록을 복사할 때 동작합니다.

    개발 환경


    응용 프로그램의 초기 형태를 만들기 위해 vue/cli를 설치합니다.
    node v15.5.0
    npm 7.3.0
    
    npm install -g @vue/cli @vue/cli-service-global
    # or
    yarn global add @vue/cli @vue/cli-service-global
    
    다음에 응용 프로그램의 모형을 만든다.
    vue create vue3-firebase-demo
    
    Vue3 및 Type Script를 사용하여 설명서 선택을 선택합니다.Type Script/Router를 추가로 선택하고 결정하십시오.그런 다음 Vue2 또는 3을 사용하는지 묻습니다. 3을 선택하십시오.나머지는 오락대로만.
    결국 이렇게 될 것 같아서요.그러면 오락으로 모형을 만들어 보자.
    ? Please pick a preset: Manually select features
    ? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Linter
    ? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
    ? Use class-style component syntax? No
    ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
    ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
    ? Pick a linter / formatter config: Basic
    ? Pick additional lint features: Lint on save
    ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
    ? Save this as a preset for future projects? No
    

    npm ERR! code ERR_SOCKET_TIMEOUT


    필자의 환경(win10+wsl2+ubuntu20)에서 상술한 오류가 발생하여 실행하였다sudo vue create vue3-firebase-demo.

    UI 만들기


    UI를 구성하는 구성 요소를 만듭니다.

    1. 머리글



    머리글은 로그인 상태에 접근하는 구성 요소 중 하나입니다.

    components에 파일 추가


    src/components/LoginButton.vue
    LoginButton.vue
    <template>
      <button type="button" class="google-button">
        <span class="google-button__icon">
          <svg viewBox="0 0 366 372" xmlns="http://www.w3.org/2000/svg">
            <path
              d="M125.9 10.2c40.2-13.9 85.3-13.6 125.3 1.1 22.2 8.2 42.5 21 59.9 37.1-5.8 6.3-12.1 12.2-18.1 18.3l-34.2 34.2c-11.3-10.8-25.1-19-40.1-23.6-17.6-5.3-36.6-6.1-54.6-2.2-21 4.5-40.5 15.5-55.6 30.9-12.2 12.3-21.4 27.5-27 43.9-20.3-15.8-40.6-31.5-61-47.3 21.5-43 60.1-76.9 105.4-92.4z"
              id="Shape"
              fill="#EA4335"
            />
            <path
              d="M20.6 102.4c20.3 15.8 40.6 31.5 61 47.3-8 23.3-8 49.2 0 72.4-20.3 15.8-40.6 31.6-60.9 47.3C1.9 232.7-3.8 189.6 4.4 149.2c3.3-16.2 8.7-32 16.2-46.8z"
              id="Shape"
              fill="#FBBC05"
            />
            <path
              d="M361.7 151.1c5.8 32.7 4.5 66.8-4.7 98.8-8.5 29.3-24.6 56.5-47.1 77.2l-59.1-45.9c19.5-13.1 33.3-34.3 37.2-57.5H186.6c.1-24.2.1-48.4.1-72.6h175z"
              id="Shape"
              fill="#4285F4"
            />
            <path
              d="M81.4 222.2c7.8 22.9 22.8 43.2 42.6 57.1 12.4 8.7 26.6 14.9 41.4 17.9 14.6 3 29.7 2.6 44.4.1 14.6-2.6 28.7-7.9 41-16.2l59.1 45.9c-21.3 19.7-48 33.1-76.2 39.6-31.2 7.1-64.2 7.3-95.2-1-24.6-6.5-47.7-18.2-67.6-34.1-20.9-16.6-38.3-38-50.4-62 20.3-15.7 40.6-31.5 60.9-47.3z"
              fill="#34A853"
            />
          </svg>
        </span>
        <span class="google-button__text">Sign in with Google</span>
      </button>
    </template>
    
    <script lang='ts'>
    import { defineComponent } from "vue";
    
    export default defineComponent({
      name: "LoginButton",
    });
    </script>
    
    <style scoped>
    .google-button {
      height: 40px;
      border-width: 0;
      border-radius: 5px;
      white-space: nowrap;
      box-shadow: 1px 1px 0px 1px rgba(0, 0, 0, 0.05);
      transition-property: background-color, box-shadow;
      transition-duration: 150ms;
      transition-timing-function: ease-in-out;
      padding: 0;
    }
    .google-button__icon {
      display: inline-block;
      vertical-align: middle;
      margin: 8px 0 8px 8px;
      width: 18px;
      height: 18px;
      box-sizing: border-box;
    }
    .google-button__icon--plus {
      width: 27px;
    }
    .google-button__text {
      display: inline-block;
      vertical-align: middle;
      padding: 0 24px;
      font-size: 14px;
      font-weight: bold;
      font-family: "Roboto", arial, sans-serif;
    }
    </style>
    
    src/components/User.vue
    User.vue
    <template>
      <div class="user" v-if="true">
        <img class="photoURL" src="" alt="" />
        <h3 class="displayName">Guest</h3>
        <button
          type="button"
          class="button is-small is-info is-outlined"
        >
          Sign out
        </button>
      </div>
      <div class="user" v-else>
        <LoginButton />
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import LoginButton from "@/components/LoginButton.vue";
    
    export default defineComponent({
      name: "User",
      components: {
        LoginButton,
      },
    });
    </script>
    
    <style scoped>
    .user {
      margin-left: auto;
      height: 50px;
      display: inline-flex;
      align-items: center;
      flex-wrap: wrap;
    }
    .displayName,
    button {
      white-space: nowrap;
    }
    .displayName {
      margin: 0 20px 0 10px;
    }
    button {
      font-weight: 600;
    }
    .photoURL {
      border-radius: 50%;
      object-fit: cover;
      width: 50px;
      height: 50px;
    }
    </style>
    
    src/components/Header.vue
    Header.vue
    <template>
      <div class="header">
        <router-link to="/">Home</router-link>
        <p>/</p>
        <router-link to="/setting">Setting</router-link>
        <User />
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import User from "@/components/User.vue";
    
    export default defineComponent({
      name: "Header",
      components: {
        User,
      },
    });
    </script>
    
    <style scoped>
    .header {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      padding: 10px 0;
    }
    p {
      margin: 0 5px;
    }
    </style>
    

    응용 프로그램에 머리글 추가


    이 응용 프로그램은 Water.css를 사용하기 위해 App.vue로 읽습니다.
    src/App.vue
    App.vue
    <template>
      <Header />
      <router-view />
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import Header from "@/components/Header.vue";
    
    export default defineComponent({
      name: "App",
      components: {
        Header,
      },
    });
    </script>
    
    <style>
    @import "https://cdn.jsdelivr.net/npm/water.css@2/out/dark.min.css";
    </style>
    

    2. 홈 화면


    UI를 준비하여 응용 프로그램에서 처음 표시되는 화면에 로그인한 사람의 이름을 표시합니다.

    src/views/Home.vue
    Home.vue
    <template>
      <div class="hello">
        <h1>Hello, {{ guest }}</h1>
        <p>
          This is a example of an article.
        </p>
        <p>
          Vue3 + Composition API + store pattern + Firebase Auth
        </p>
        <h3>Related links</h3>
        <ul>
          <li><a href="https://github.com/ErgoFriend/vue-firebase-authl" target="_blank" rel="noopener">GitHub Repo</a></li>
          <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">Zenn Article</a></li>
        </ul>
        <br>
        <p>Copyright © 2021 <a href="https://kasu.dev">kasu.dev</a> All Rights Reserved.</p>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from 'vue';
    
    export default defineComponent({
      name: 'Home',
      setup(){
        const guest = '{{ Guest }}'
        return { guest }
      }
    });
    </script>
    
    <style scoped>
    h3 {
      margin: 40px 0 0;
    }
    ul {
      list-style-type: none;
      padding: 0;
    }
    li {
      display: inline-block;
      margin: 0 10px;
    }
    a {
      color: #42b983;
    }
    </style>
    

    3. 화면 설정


    로그인자의 사용자 이름과 프로필 이미지를 변경할 페이지를 준비합니다.
    이 프로그램으로 로그인 상태에 접근하는 구성 요소는 머리, 홈 화면, 설정 화면 3개입니다.

    src/views/Setting.vue
    Setting.vue
    <template>
      <div class="view">
        <h1>Setting</h1>
        <div class="updater">
          <label>displayName</label>
          <input type="text" v-model="displayName" />
        </div>
        <div class="updater">
          <label>photoURL</label>
          <input type="text" v-model="photoURL" />
        </div>
        <button>Update</button>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent, ref, watchEffect } from "vue";
    
    export default defineComponent({
      name: "Setting",
      setup() {
        const displayName = ref("");
        const photoURL = ref("");
        return { displayName, photoURL };
      },
    });
    </script>
    
    <style scoped>
    .updater {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
    }
    label {
      flex: 1;
    }
    input {
      flex: 3;
    }
    button {
      float: right;
    }
    </style>
    

    4. 라우팅


    홈 화면과 설정 화면을 이동할 수 있도록 vue-router 설정합니다.
    src/router/index.ts
    index.ts
    import { createWebHistory, createRouter } from "vue-router";
    import Home from "@/views/Home.vue";
    import Setting from "@/views/Setting.vue";
    
    const routes = [
      {
        path: "/",
        name: "Home",
        component: Home,
      },
      {
        path: "/setting",
        name: "Setting",
        component: Setting,
      }
    ];
    
    const router = createRouter({
      history: createWebHistory(),
      routes,
    });
    
    export default router;
    

    상점 만들기


    로그인 상태를 관리하는 스토어


    관리 로그인 상태를 만듭니다authStore.
    로그인 상태를 유지하는 구성 요소에 등록authStore, 로그인 상태를 이용한 구성 요소에서 호출useAuthStore(), 로그인된authStore에서 데이터를 읽고 호출하는 방법.
    src/stores/auth.ts
    auth.ts
    import { inject, InjectionKey, reactive } from "vue";
    
    type DummyUser = {
      displayName: string;
      photoURL: string;
    }
    
    const dummyUser: DummyUser = {
      displayName: 'test',
      photoURL: 'https://pbs.twimg.com/profile_images/1308010958862905345/-SGZioPb_400x400.jpg'
    }
    
    const authStore = () => {
      console.log('init authStore')
      const state = reactive({ isLoggedin: false, displayName: '', photoURL: ''})
      const setUser = (user: DummyUser | null) => {
        state.isLoggedin = !!user
        if (user) {
          state.displayName = user.displayName ?? ''
          state.photoURL = user.photoURL ?? ''
        }
      }
      const signin = () => {
        setUser(dummyUser)
      }
      const signout = () => setUser(null)
      const updateUser = (input: DummyUser) => {
        setUser(input)
      }
    
      return {
        state,
        setUser,
        signin,
        signout,
        updateUser,
      };
    }
    
    export default authStore
    
    export type AuthStore = ReturnType<typeof authStore>;
    
    export const authStoreKey: InjectionKey<AuthStore> = Symbol('authStore');
    
    export const useAuthStore = () => {
      const store = inject(authStoreKey);
      if (!store) {
        throw new Error(`${authStoreKey} is not provided`);
      }
      return store;
    }
    

    스토어 연결


    상점 모드에서 상점을 사용할 구성 요소에 연결합니다.이번에는 전체 프로그램에서 로그인 상태를 사용하기 위해 루트App로provide를 진행 중입니다.
    src/main.ts
    main.ts
    import { createApp } from 'vue'
    import App from '@/App.vue'
    import router from '@/router'
    import authStore, { authStoreKey } from '@/stores/auth'
    
    const app = createApp(App)
    app.use(router)
    app.provide(authStoreKey, authStore())
    app.mount('#app')
    

    상점의 호출


    각 구성 요소에서 로그인 상태에 액세스합니다.
    매장에서는 authStore inject 후useAuthStore()를 다음과 같이 사용합니다.
    사용 예
    const { state } = useAuthStore();
    console.log('name:', state.displayName)
    
    src/stores/auth.ts
    export const useAuthStore = () => {
      const store = inject(authStoreKey);
      if (!store) {
        throw new Error(`${authStoreKey} is not provided`);
      }
      return store;
    }
    

    1. 머리글


    LoginButton.vue
    로그인 단추를 누르면authStore의 로그인 함수signin()를 실행합니다.
    src/components/LoginButton.vue
    LoginButton.vue
    <template>
      <button type="button" class="google-button" @click="signin()">
        <span class="google-button__icon">
          <svg viewBox="0 0 366 372" xmlns="http://www.w3.org/2000/svg">
            <path
              d="M125.9 10.2c40.2-13.9 85.3-13.6 125.3 1.1 22.2 8.2 42.5 21 59.9 37.1-5.8 6.3-12.1 12.2-18.1 18.3l-34.2 34.2c-11.3-10.8-25.1-19-40.1-23.6-17.6-5.3-36.6-6.1-54.6-2.2-21 4.5-40.5 15.5-55.6 30.9-12.2 12.3-21.4 27.5-27 43.9-20.3-15.8-40.6-31.5-61-47.3 21.5-43 60.1-76.9 105.4-92.4z"
              id="Shape"
              fill="#EA4335"
            />
            <path
              d="M20.6 102.4c20.3 15.8 40.6 31.5 61 47.3-8 23.3-8 49.2 0 72.4-20.3 15.8-40.6 31.6-60.9 47.3C1.9 232.7-3.8 189.6 4.4 149.2c3.3-16.2 8.7-32 16.2-46.8z"
              id="Shape"
              fill="#FBBC05"
            />
            <path
              d="M361.7 151.1c5.8 32.7 4.5 66.8-4.7 98.8-8.5 29.3-24.6 56.5-47.1 77.2l-59.1-45.9c19.5-13.1 33.3-34.3 37.2-57.5H186.6c.1-24.2.1-48.4.1-72.6h175z"
              id="Shape"
              fill="#4285F4"
            />
            <path
              d="M81.4 222.2c7.8 22.9 22.8 43.2 42.6 57.1 12.4 8.7 26.6 14.9 41.4 17.9 14.6 3 29.7 2.6 44.4.1 14.6-2.6 28.7-7.9 41-16.2l59.1 45.9c-21.3 19.7-48 33.1-76.2 39.6-31.2 7.1-64.2 7.3-95.2-1-24.6-6.5-47.7-18.2-67.6-34.1-20.9-16.6-38.3-38-50.4-62 20.3-15.7 40.6-31.5 60.9-47.3z"
              fill="#34A853"
            />
          </svg>
        </span>
        <span class="google-button__text">Sign in with Google</span>
      </button>
    </template>
    
    <script lang='ts'>
    import { defineComponent } from "vue";
    import { useAuthStore } from "@/stores/auth";
    export default defineComponent({
      name: "LoginButton",
      setup() {
        const { signin } = useAuthStore();
        return { signin };
      },
    });
    </script>
    
    <style scoped>
    .google-button {
      height: 40px;
      border-width: 0;
      border-radius: 5px;
      white-space: nowrap;
      box-shadow: 1px 1px 0px 1px rgba(0, 0, 0, 0.05);
      transition-property: background-color, box-shadow;
      transition-duration: 150ms;
      transition-timing-function: ease-in-out;
      padding: 0;
    }
    .google-button__icon {
      display: inline-block;
      vertical-align: middle;
      margin: 8px 0 8px 8px;
      width: 18px;
      height: 18px;
      box-sizing: border-box;
    }
    .google-button__icon--plus {
      width: 27px;
    }
    .google-button__text {
      display: inline-block;
      vertical-align: middle;
      padding: 0 24px;
      font-size: 14px;
      font-weight: bold;
      font-family: "Roboto", arial, sans-serif;
    }
    </style>
    
    User.vue
    로그인할 때 개인 프로필 이미지와 이름을 표시합니다. 그렇지 않으면 로그인 단추를 표시합니다.
    src/components/User.vue
    User.vue
    <template>
      <div class="user" v-if="state.isLoggedin">
        <img class="photoURL" :src="state.photoURL" alt="" />
        <h3 class="displayName">{{ state.displayName }}</h3>
        <button
          type="button"
          class="button is-small is-info is-outlined"
          @click="signout()"
        >
          Sign out
        </button>
      </div>
      <div class="user" v-else>
        <LoginButton />
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import { useAuthStore } from "@/stores/auth";
    import LoginButton from "@/components/LoginButton.vue";
    
    export default defineComponent({
      name: "User",
      components: {
        LoginButton,
      },
      setup() {
        const { signout, state } = useAuthStore();
        return {
          state,
          signout,
        };
      },
    });
    </script>
    
    <style scoped>
    .user {
      margin-left: auto;
      height: 50px;
      display: inline-flex;
      align-items: center;
      flex-wrap: wrap;
    }
    .displayName,
    button {
      white-space: nowrap;
    }
    .displayName {
      margin: 0 20px 0 10px;
    }
    button {
      font-weight: 600;
    }
    .photoURL {
      border-radius: 50%;
      object-fit: cover;
      width: 50px;
      height: 50px;
    }
    </style>
    

    2. 홈 화면


    로그인자의 이름을 표시합니다.
    src/views/Home.vue
    Home.vue
    <template>
      <div class="hello">
        <h1>Hello, {{ state.isLoggedin ? state.displayName : guest }}</h1>
        <p>
          This is a example of an article.
        </p>
        <p>
          Vue3 + Composition API + store pattern + Firebase Auth
        </p>
        <h3>Related links</h3>
        <ul>
          <li><a href="https://github.com/ErgoFriend/vue-firebase-authl" target="_blank" rel="noopener">GitHub Repo</a></li>
          <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">Zenn Article</a></li>
        </ul>
        <br>
        <p>Copyright © 2021 <a href="https://kasu.dev">kasu.dev</a> All Rights Reserved.</p>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from 'vue';
    import { useAuthStore } from '@/stores/auth';
    
    export default defineComponent({
      name: 'Home',
      setup(){
        const guest = '{{ Guest }}'
        const { state } = useAuthStore()
        return { guest, state }
      }
    });
    </script>
    
    <style scoped>
    h3 {
      margin: 40px 0 0;
    }
    ul {
      list-style-type: none;
      padding: 0;
    }
    li {
      display: inline-block;
      margin: 0 10px;
    }
    a {
      color: #42b983;
    }
    </style>
    

    3. 화면 설정


    로그인자의 이름과 이미지 URL을 변경할 수 있습니다.
    우선 빈 문자열로 초기값을 입력하는 v-model을 준비합니다.이후watchEffect 상점을 감시하여 값을 동기화합니다.
    src/views/Setting.vue
    Setting.vue
    <template>
      <div class="view">
        <h1>Setting</h1>
        <div class="updater">
          <label>displayName</label>
          <input type="text" v-model="displayName" />
        </div>
        <div class="updater">
          <label>photoURL</label>
          <input type="text" v-model="photoURL" />
        </div>
        <button @click="update()">Update</button>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent, ref, watchEffect } from "vue";
    import { useAuthStore } from "@/stores/auth";
    
    export default defineComponent({
      name: "Setting",
      setup() {
        const { state, updateUser } = useAuthStore();
        const displayName = ref("");
        const photoURL = ref("");
        const update = () =>
          updateUser({ displayName: displayName.value, photoURL: photoURL.value });
    
        watchEffect(() => {
          displayName.value = state.displayName;
          photoURL.value = state.photoURL;
        });
    
        return { displayName, photoURL, update };
      },
    });
    </script>
    
    <style scoped>
    .updater {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
    }
    label {
      flex: 1;
    }
    input {
      flex: 3;
    }
    button {
      float: right;
    }
    </style>
    

    Firebase 추가


    응용 프로그램에 Firebase 패키지를 추가합니다.
    npm install --save firebase
    or
    yarn add firebase
    

    설정


    Firebase를 응용 프로그램에 연결합니다.main.ts Firebase 설정이 진행 중입니다.configFirebase의 프로젝트 설정 화면에 웹을 추가한 제 응용 프로그램에서 얻습니다.https://console.firebase.google.com/
    src/main.ts
    main.ts
    import { createApp } from 'vue'
    import firebase from "firebase/app"
    import 'firebase/auth'
    import App from '@/App.vue'
    import router from '@/router'
    import authStore, { authStoreKey } from '@/stores/auth'
    
    const config = {
      apiKey: "AIzaSyC65X-Nz7uVsrPR1x6rI2tx_Mjdny6pAl8",
      authDomain: "vue-firebase-demo-ebdef.firebaseapp.com",
      projectId: "vue-firebase-demo-ebdef",
      storageBucket: "vue-firebase-demo-ebdef.appspot.com",
      messagingSenderId: "213709504759",
      appId: "1:213709504759:web:40aac8e6d6cc38cbeb53f2",
      measurementId: "G-EVD8ZEB5M3"
    }
    
    firebase.initializeApp(config);
    firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
    
    const app = createApp(App)
    app.use(router)
    app.provide(authStoreKey, authStore())
    app.mount('#app')
    

    응용 프로그램 가져오기

    siginin()·siginin()에서 실행firebase.auth(), 로그인 상태가 로그인·로그아웃을 이용할 때 호칭firebase.auth().onAuthStateChanged에서 setUser()state를 변경합니다.
    src/stores/auth.ts
    auth.ts
    import firebase from "firebase/app";
    import 'firebase/auth';
    import { inject, InjectionKey, reactive } from "vue";
    
    const authStore = () => {
      console.log('init authStore')
      const state = reactive({ isLoggedin: false, displayName: '', photoURL: ''})
      const setUser = (user: firebase.User | null) => {
        state.isLoggedin = !!user
        if (user) {
          state.displayName = user.displayName ?? ''
          state.photoURL = user.photoURL ?? ''
        }
      }
      const signin = () => {
        const provider = new firebase.auth.GoogleAuthProvider();
        firebase.auth().signInWithPopup(provider)
      }
      const signout = () => firebase.auth().signOut()
      const updateUser = (input: { displayName?: string; photoURL?: string }) => {
        firebase.auth().currentUser?.updateProfile(input)
          .then(() => setUser(firebase.auth().currentUser)
        )
      }
    
      firebase.auth().onAuthStateChanged((user) => setUser(user))
    
      return {
        state,
        setUser,
        signin,
        signout,
        updateUser,
      };
    }
    
    export default authStore
    
    export type AuthStore = ReturnType<typeof authStore>;
    
    export const authStoreKey: InjectionKey<AuthStore> = Symbol('authStore');
    
    export const useAuthStore = () => {
      const store = inject(authStoreKey);
      if (!store) {
        throw new Error(`${authStoreKey} is not provided`);
      }
      return store;
    }
    
    이 응용 프로그램은 여기까지만 하면 완성된다.이번에는 상점 모드로 로그인 상태를 관리하는 앱을 만들었다.데모로 이동하지 않을 경우 GiitHub 웨어하우스를 참조하거나 Discussion으로 보내십시오.

    끝말


    끝까지 읽어주셔서 감사합니다.
    필자의 최초의 프로그래밍 경험은 Vue였고, 그 후에 바로 React를 하기 시작했지만, 나는 당시에 매우 고생했던 것을 기억한다.Composition API 시대의 Vue에서 React를 시작하면 비교적 가벼운 느낌도 든다.

    좋은 웹페이지 즐겨찾기