본문 바로가기
카테고리 없음

로그인한 정보를 vue에 보관했다가 필요할 때 사용하려면 어떻게 해?

by 두잇건강 2025. 8. 20.
반응형

Vue.js에서 로그인한 사용자 정보를 전역적으로 보관하고 사용하는 여러 방법을 제공해드리겠습니다. Vue 3에서는 **Pinia**, **Composables**, **provide/inject** 등을 활용할 수 있습니다.

## 방법 1: Pinia 상태 관리 (권장)

### 1. Pinia 설치 및 설정

```bash
npm install pinia
```

**main.js**

```javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import axios from 'axios'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.use(router)

// Axios 설정
axios.defaults.baseURL = 'http://localhost:8080'

app.mount('#app')
```

### 2. 사용자 정보 Store 생성

**stores/auth.js**

```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'

export const useAuthStore = defineStore('auth', () => {
  // 상태 (State)
  const user = ref(null)
  const token = ref(localStorage.getItem('jwt-token'))
  const loading = ref(false)
  const error = ref('')

  // 계산된 속성 (Getters)
  const isAuthenticated = computed(() => !!token.value && !!user.value)
  const userEmail = computed(() => user.value?.email || '')
  const userName = computed(() => user.value?.name || '')
  const userRoles = computed(() => user.value?.roles || [])
  const isAdmin = computed(() => userRoles.value.includes('ROLE_ADMIN'))
  const isManager = computed(() => userRoles.value.includes('ROLE_MANAGER'))

  // 액션 (Actions)
  const login = async (credentials) => {
    loading.value = true
    error.value = ''
    
    try {
      const response = await axios.post('/api/authenticate', credentials)
      
      if (response.data.jwt) {
        // 토큰 저장
        token.value = response.data.jwt
        localStorage.setItem('jwt-token', response.data.jwt)
        
        // axios 헤더 설정
        setAuthToken(response.data.jwt)
        
        // 사용자 정보 저장
        user.value = {
          email: response.data.email || credentials.email,
          name: response.data.name || '',
          roles: response.data.roles || ['ROLE_USER'],
          id: response.data.userId || null
        }
        
        // 사용자 정보를 localStorage에도 저장 (새로고침 대응)
        localStorage.setItem('user-info', JSON.stringify(user.value))
        
        return { success: true }
      }
    } catch (err) {
      error.value = err.response?.status === 401
        ? '이메일 또는 비밀번호가 올바르지 않습니다.'
        : '로그인 중 오류가 발생했습니다.'
      return { success: false, error: error.value }
    } finally {
      loading.value = false
    }
  }

  const logout = async () => {
    loading.value = true
    
    try {
      // 서버에 로그아웃 요청
      await axios.post('/api/logout')
    } catch (err) {
      console.error('로그아웃 API 호출 실패:', err)
    } finally {
      // 로컬 상태 정리
      clearAuthData()
      loading.value = false
    }
  }

  const fetchUserProfile = async () => {
    if (!token.value) return
    
    loading.value = true
    try {
      const response = await axios.get('/api/user/profile')
      user.value = {
        ...user.value,
        ...response.data
      }
      localStorage.setItem('user-info', JSON.stringify(user.value))
    } catch (err) {
      console.error('사용자 프로필 가져오기 실패:', err)
      if (err.response?.status === 401) {
        clearAuthData()
      }
    } finally {
      loading.value = false
    }
  }

  const updateUserProfile = async (profileData) => {
    loading.value = true
    try {
      const response = await axios.put('/api/user/profile', profileData)
      user.value = { ...user.value, ...response.data }
      localStorage.setItem('user-info', JSON.stringify(user.value))
      return { success: true }
    } catch (err) {
      error.value = '프로필 업데이트 중 오류가 발생했습니다.'
      return { success: false, error: error.value }
    } finally {
      loading.value = false
    }
  }

  const clearAuthData = () => {
    user.value = null
    token.value = null
    error.value = ''
    localStorage.removeItem('jwt-token')
    localStorage.removeItem('user-info')
    delete axios.defaults.headers.common['Authorization']
  }

  const setAuthToken = (authToken) => {
    if (authToken) {
      axios.defaults.headers.common['Authorization'] = `Bearer ${authToken}`
    } else {
      delete axios.defaults.headers.common['Authorization']
    }
  }

  const initializeAuth = () => {
    const savedToken = localStorage.getItem('jwt-token')
    const savedUser = localStorage.getItem('user-info')
    
    if (savedToken && savedUser) {
      token.value = savedToken
      user.value = JSON.parse(savedUser)
      setAuthToken(savedToken)
      
      // 사용자 정보 갱신 (선택적)
      fetchUserProfile()
    }
  }

  return {
    // 상태
    user,
    token,
    loading,
    error,
    
    // 계산된 속성
    isAuthenticated,
    userEmail,
    userName,
    userRoles,
    isAdmin,
    isManager,
    
    // 액션
    login,
    logout,
    fetchUserProfile,
    updateUserProfile,
    clearAuthData,
    initializeAuth
  }
})
```

### 3. 컴포넌트에서 사용하기

**Login.vue**

```vue
<template>
  <div class="login-container">
    <div class="login-form">
      <h2>로그인</h2>
      <form @submit.prevent="handleLogin">
        <div class="form-group">
          <label for="email">이메일:</label>
          <input
            type="email"
            id="email"
            v-model="loginForm.email"
            required
            placeholder="이메일을 입력하세요"
          />
        </div>
        <div class="form-group">
          <label for="password">비밀번호:</label>
          <input
            type="password"
            id="password"
            v-model="loginForm.password"
            required
            placeholder="비밀번호를 입력하세요"
          />
        </div>
        <button type="submit" :disabled="authStore.loading" class="login-btn">
          {{ authStore.loading ? '로그인 중...' : '로그인' }}
        </button>
      </form>
      <div v-if="authStore.error" class="error-message">
        {{ authStore.error }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = useRouter()
const authStore = useAuthStore()

const loginForm = reactive({
  email: '',
  password: ''
})

const handleLogin = async () => {
  const result = await authStore.login(loginForm)
  
  if (result.success) {
    await router.push('/dashboard')
  }
}
</script>
```

**Dashboard.vue**

```vue
<template>
  <div class="dashboard-container">
    <header class="dashboard-header">
      <div class="user-info">
        <h1>대시보드</h1>
        <p>환영합니다, {{ authStore.userName || authStore.userEmail }}님!</p>
      </div>
      <div class="header-actions">
        <span class="user-roles">
          권한: {{ authStore.userRoles.join(', ') }}
        </span>
        <button @click="handleLogout" class="logout-btn" :disabled="authStore.loading">
          {{ authStore.loading ? '로그아웃 중...' : '로그아웃' }}
        </button>
      </div>
    </header>
    
    <main class="dashboard-content">
      <div class="user-profile">
        <h3>사용자 정보</h3>
        <p><strong>이메일:</strong> {{ authStore.userEmail }}</p>
        <p><strong>이름:</strong> {{ authStore.userName }}</p>
        <p><strong>권한:</strong> {{ authStore.userRoles.join(', ') }}</p>
        <p><strong>관리자 여부:</strong> {{ authStore.isAdmin ? 'Yes' : 'No' }}</p>
        <p><strong>매니저 여부:</strong> {{ authStore.isManager ? 'Yes' : 'No' }}</p>
      </div>
      
      <!-- 관리자만 볼 수 있는 섹션 -->
      <div v-if="authStore.isAdmin" class="admin-section">
        <h3>관리자 전용 기능</h3>
        <button @click="adminFunction" class="admin-btn">
          관리자 기능 실행
        </button>
      </div>
      
      <!-- 일반 사용자 기능 -->
      <div class="user-actions">
        <button @click="fetchProfile" class="action-btn" :disabled="authStore.loading">
          프로필 새로고침
        </button>
        <button @click="updateProfile" class="action-btn">
          프로필 수정
        </button>
      </div>
    </main>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = useRouter()
const authStore = useAuthStore()

const handleLogout = async () => {
  await authStore.logout()
  router.push('/login')
}

const fetchProfile = async () => {
  await authStore.fetchUserProfile()
}

const updateProfile = async () => {
  // 프로필 수정 로직
  const profileData = {
    name: '새로운 이름'
  }
  const result = await authStore.updateUserProfile(profileData)
  if (result.success) {
    alert('프로필이 업데이트되었습니다.')
  }
}

const adminFunction = () => {
  alert('관리자 기능이 실행되었습니다!')
}
</script>
```

### 4. 라우터 가드에서 활용

**router/index.js**

```javascript
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import Login from '@/components/Login.vue'
import Dashboard from '@/components/Dashboard.vue'
import AdminPanel from '@/components/AdminPanel.vue'

const routes = [
  {
    path: '/',
    redirect: '/dashboard'
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    name: 'AdminPanel',
    component: AdminPanel,
    meta: {
      requiresAuth: true,
      requiresAdmin: true
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()
  
  // 인증이 필요한 페이지
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next('/login')
    return
  }
  
  // 관리자 권한이 필요한 페이지
  if (to.meta.requiresAdmin && !authStore.isAdmin) {
    next('/dashboard') // 또는 권한 없음 페이지
    return
  }
  
  // 이미 로그인한 사용자가 로그인 페이지 접근 시
  if (to.path === '/login' && authStore.isAuthenticated) {
    next('/dashboard')
    return
  }
  
  next()
})

export default router
```

### 5. App.vue에서 초기화

**App.vue**

```vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'

const authStore = useAuthStore()

onMounted(() => {
  // 앱 시작 시 인증 상태 초기화
  authStore.initializeAuth()
})
</script>
```

## 방법 2: Composables 방식 (더 간단한 방법)

**composables/useAuthStore.js**

```javascript
import { ref, computed, reactive } from 'vue'
import axios from 'axios'

// 전역 상태
const state = reactive({
  user: JSON.parse(localStorage.getItem('user-info')) || null,
  token: localStorage.getItem('jwt-token') || null,
  loading: false,
  error: ''
})

export function useAuthStore() {
  // 계산된 속성
  const isAuthenticated = computed(() => !!state.token && !!state.user)
  const userInfo = computed(() => state.user)
  const isAdmin = computed(() => state.user?.roles?.includes('ROLE_ADMIN'))

  // 사용자 정보 업데이트
  const setUser = (userData) => {
    state.user = userData
    localStorage.setItem('user-info', JSON.stringify(userData))
  }

  // 토큰 설정
  const setToken = (token) => {
    state.token = token
    if (token) {
      localStorage.setItem('jwt-token', token)
      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
    } else {
      localStorage.removeItem('jwt-token')
      delete axios.defaults.headers.common['Authorization']
    }
  }

  // 로그인
  const login = async (credentials) => {
    state.loading = true
    state.error = ''
    
    try {
      const response = await axios.post('/api/authenticate', credentials)
      
      if (response.data.jwt) {
        setToken(response.data.jwt)
        setUser({
          email: response.data.email || credentials.email,
          name: response.data.name || '',
          roles: response.data.roles || ['ROLE_USER']
        })
        
        return { success: true }
      }
    } catch (error) {
      state.error = '로그인에 실패했습니다.'
      return { success: false, error: state.error }
    } finally {
      state.loading = false
    }
  }

  // 로그아웃
  const logout = () => {
    setToken(null)
    setUser(null)
    state.error = ''
  }

  return {
    // 상태
    user: computed(() => state.user),
    loading: computed(() => state.loading),
    error: computed(() => state.error),
    
    // 계산된 속성
    isAuthenticated,
    userInfo,
    isAdmin,
    
    // 메서드
    login,
    logout,
    setUser,
    setToken
  }
}
```

## 방법 3: provide/inject 방식

**App.vue**

```vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script setup>
import { provide, reactive, computed } from 'vue'

const authState = reactive({
  user: JSON.parse(localStorage.getItem('user-info')) || null,
  token: localStorage.getItem('jwt-token') || null
})

const auth = {
  state: authState,
  isAuthenticated: computed(() => !!authState.token && !!authState.user),
  userInfo: computed(() => authState.user),
  
  setUser(userData) {
    authState.user = userData
    localStorage.setItem('user-info', JSON.stringify(userData))
  },
  
  clearAuth() {
    authState.user = null
    authState.token = null
    localStorage.removeItem('user-info')
    localStorage.removeItem('jwt-token')
  }
}

provide('auth', auth)
</script>
```

**컴포넌트에서 사용**

```vue
<script setup>
import { inject } from 'vue'

const auth = inject('auth')

// 사용 예시
console.log(auth.userInfo.value)
console.log(auth.isAuthenticated.value)
</script>
```

## 권장사항

**Pinia 사용을 권장하는 이유:**
- Vue 3 공식 상태 관리 라이브러리
- 타입스크립트 지원 우수
- 개발자 도구 지원
- 모듈화된 상태 관리
- SSR 지원

**사용자 정보 보관 전략:**
- 민감하지 않은 정보만 클라이언트에 저장
- 토큰은 httpOnly 쿠키 사용 고려
- 새로고침 시 상태 복구 로직 구현
- 권한 기반 UI 렌더링

이렇게 구현하면 Vue.js 애플리케이션 전체에서 로그인한 사용자 정보를 쉽게 접근하고 활용할 수 있습니다.

출처

반응형