티스토리 뷰

prod/Nuxt.js

Nuxt 사용기 (1)

dev_0hoon 2025. 3. 17. 17:46

개인 운동기록 & pt 매칭 프로그램을 만들며 Nuxt를 사용하게 됐다. 몇 주간 Nuxt을 개인적으로 공부하며 잡힌 사용 방법에 대해 기록한다.

 

 

✅ 파일구조

https://nuxt.com/docs/guide/directory-structure/layouts#named-layout

1️⃣ 서문

나는 백엔드를 주로 사용하던 개발자로써 기존 아키텍처에 대해 익숙하다

- 컨트롤러 -> 서비스 -> 맵퍼

- 컨트롤러 -> 서비스 -> 레파지토리 

그리고 데이터를 운반하는 dto, vo, entity의 개념이 익숙하다. 

 

1) domain별로 컨트롤러,서비스,레파지토리, dto, entity로 레이어드 아키텍처 파일구조 이거나

2) presentation, domain, infrastructure 로 클린 레이어드 아키텍처의 파일구조

2가지를 주로 사용한다.

 

2️⃣ Nuxt의 파일구조에 대한 생각

기존 vue에 대한 개념을 잡고 싶어 인프런의 캡틴 판교의 강의를 주로 참고 했다

 

💫생각 정리

1단계

<template>
  <!-- 전체 컨테이너 -->
  <div class="container">
    <!-- 상단 타이틀 (검색창 포함) -->
    <ManageHeader></ManageHeader>
    <!-- 카테고리 메뉴 -->
    <MemberList></MemberList>
    <!-- 하단 고정 푸터 -->
    <footer class="fixed-footer">
      <div v-for="item in category" @click="selectCategory(item)" class="footer-item" :class="{active : selectedCategory == item}" >{{ item }}</div>
    </footer>
  </div>
</template>

<script setup lang="ts">
  import ManageHeader from "~/components/user/manage/ManageHeader.vue";
  import {api} from "~/store/api";
  import {useMemberStore} from "~/store/member";
  import MemberList from "~/components/user/trainer/MemberList.vue";

  const memberStore = useMemberStore();
  const category = ref(['회원관리','마이페이지']);
  const selectedCategory = ref('회원관리');

  const selectCategory = (category) => {
    selectedCategory.value = category;
  }


</script>

<style scoped>

</style>

- page: page폴더에 아래에 들어오는 vue파일들은 자동으로 path에 연결된다.

1) / 는 index.vue 

2) /user 는 user폴더 아래의 index.vue

3) /user/list는 user폴더 아래의 list.vue

4) /user/1 은 user폴더 아래의 [id].vue을 가지며 파라미터의 값은 useRouter를 사용해 값을 꺼내게 된다.

 

<template>
  <div>
    <div class="default-nav">
      <div v-if="memberStore.useFilter">총 {{memberStore.filteredMembersCount}}명</div>
      <div v-if="!memberStore.useFilter">총 {{memberStore.membersCount}}명</div>
      <div class="default-nav-item">
        <button @click.stop="clickSort" class="sort-btn">
          <img src="@/assets/icons//swap-vertical.svg" alt="">
          <span>
                정렬
          </span>
        </button>
      </div>
    </div>

    <div class="user-container">
      <!-- 회원이 없을 경우 표시될 문구 -->
      <div style="display: none;" class="no-users-message">등록하신 회원이 없습니다.</div>

      <div class="user-item-container">
        <!-- 회원 정보 -->
        <div class="user-item" v-for="item in membersList" :key="item.id">
          <NuxtLink :to="`/record/${item.userId}`" class="user-info">
            <div class="user-image">
              <img src="@/assets/images/1.png" alt="운동 아이콘">
            </div>
            <div class="user-text">
              <div class="user-title">{{item.userName}}</div>
              <div v-if="!item.isNew" class="user-descript gray-text">{{item.age}}세 | 마지막수업: {{item.latestCreatedAt}}</div>
              <div v-if="item.isNew" class="user-descript gray-text">{{item.age}}세 | 새로운 회원</div>
            </div>
          </NuxtLink>
        </div>
        <!--회원정보-->
      </div>
    </div>
    <!-- 정렬 모달 -->
    <div v-if="isShowSortModal" id="sortModal" class="user-sort-modal">
      <ul class="sort-options">
        <li @click="selectSort(item.sort)" v-for="item in sortTypes" class="sort-option" >{{item.name}}</li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
  import {useMemberStore} from "~/store/member";
  import {computed} from "vue";

  //data
  const memberStore = useMemberStore();
  const isShowSortModal = ref(false);
  const sortTypes = ref([
      {name: '가나다순', sort: 'userName'},
      {name: '최근 수업순', sort: 'recent'}
  ])

  //lifeCycle
  onMounted(async () => {
    const data = {sortBy : ''}
    const response = await useMember().trainerMember(data);
    memberStore.setMembers(response.result.list);
    memberStore.setMembersCount(response.result.list.length);
  });

  //method
  const selectSort = async (sort) => {
    const data = {sortBy : sort}
    const response = await useMember().trainerMember(data);
    memberStore.setMembers(response.result.list);
    memberStore.setMembersCount(response.result.list.length);
    clickSort();
  }

  const clickSort = () => {
    isShowSortModal.value = !isShowSortModal.value;
  }

  //getters
  const membersList = computed(() => {
    if(memberStore.useFilter){
      return memberStore.filteredMembers.map(m => ({
        ...m,
        isNew : m.latestCreatedAt == null ? true : false
      }))
    } else {
      return memberStore.members.map(m => ({
        ...m,
        isNew : m.latestCreatedAt == null ? true : false
      }))
    }

  })


</script>

<style scoped>

</style>

- component: page 안의 html 코드를 공통으로 나누어 관리할 경우 사용한다.

? 1) 의문인 부분이다. 공통으로 사용하지 않아도 코드상의 깔끔함을 위해서도 나누어도 될까?

?2) emit과 props을 이용해 page에서 관리되는 데이터를 운용하게 된다. 지금은 store에 필요한 것들을 넣어서 사용 중인데 이래도 되나 싶다. emit과 props를 꼭 써야하는 것일까

 

import {api} from "~/store/api";

export const useRecord  = () => {
    const recordList = async(data) => {
        try {
            const response = await api().get(`/record/list`,data);
            return response;
        } catch (e) {
            console.error("[recordList] 요청실패 : ", e);
        }
    }

    return {recordList}
}

- composable: 공통이 되는 url의 호출을 관리하게 됌

1) 예를 들어 page에서 api를 요청해야 한다면, url 호출을 만든다.

 

import {defineStore} from 'pinia'

export const useMemberStore = defineStore('member',{
    state: () => ({
        members: [],
        membersCount: 0,
        filteredMembers: [], // 검색된 회원 목록
        filteredMembersCount: 0,
        useFilter: false,
        sortBy : '',
        searchUserName : ''
    }),
    actions: {
        setMembers(data) {
            this.members = data;
            this.setMembersCount(data.length);
        },
        setMembersCount(data) {
            this.membersCount = data;
        },
        setFilteredMemberCount(data) {
            this.filteredMembersCount = data;
        },
        setSearchUserName(data) {
            this.searchUserName = data;
            this.filteredMembers = this.members.filter(m =>
                m.userName.includes(data)
            )
            let count = this.filteredMembers.length;

            this.setFilteredMemberCount(count);
        },
        setUseFilter(data) {
            this.useFilter = data;
        }
    }
})

- store: 한 섹션 안에 전역으로 사용되는 state, action 들을 관리한다.

1) action은 setState명 식으로 만들어서, page에서 조작해왔다.

?2) 컴포서블을 이곳에서 호출하고 page에서 store을 통해 값을 꺼내서 사용하거나 한다던데.. 그렇게 해도 될지 모르겠다. 전체적인 구조가 아직 좀 낯설다.

 

그럼 만약 state의 값을 불러올 때 page에서는 onMounted만 해주고 store의 함수가 state의 값을 조절하는 것인지.. 조금 의문임

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함