Vue.js 를 배워보자

Vue.js 그리드 데이터 흐름 제어: 임시 이동, 커서 위치 갱신, 롤백, 반응형 데이터 처리 A to Z

_Blue_Sky_ 2025. 3. 13. 21:23
728x90

 

 

  • Vue.js에서 그리드 데이터와 임시 데이터를 이동하는 방법
  •  Vue.js 그리드에서 현재 커서 위치 갱신 및 롤백 구현하기
  •  Vue.js의 $set과 $ 접두사의 의미 완벽 정리

     
Vue.js에서 그리드의 데이터를 다루는 예제를 보여드리겠습니다. 여기서는 한 행(row)의 데이터를 임시 데이터로 이동하고, 반대로 임시 데이터를 그리드 행으로 이동하는 예제를 작성해보겠습니다.
<template>
  <div>
    <!-- 그리드 테이블 -->
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>이름</th>
          <th>나이</th>
          <th>액션</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(item, index) in gridData" :key="item.id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.age }}</td>
          <td>
            <button @click="moveToTemp(index)">임시로 이동</button>
          </td>
        </tr>
      </tbody>
    </table>

    <!-- 임시 데이터 영역 -->
    <div v-if="tempRow" class="temp-area">
      <h3>임시 데이터</h3>
      <p>ID: {{ tempRow.id }}</p>
      <p>이름: {{ tempRow.name }}</p>
      <p>나이: {{ tempRow.age }}</p>
      <button @click="moveToGrid">그리드로 이동</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 그리드 데이터
      gridData: [
        { id: 1, name: '김철수', age: 25 },
        { id: 2, name: '이영희', age: 30 },
        { id: 3, name: '박민수', age: 35 }
      ],
      // 임시로 저장될 row 데이터
      tempRow: null
    }
  },
  methods: {
    // 그리드에서 임시로 데이터 이동
    moveToTemp(index) {
      // 선택한 행을 임시 데이터로 저장
      this.tempRow = { ...this.gridData[index] };
      // 그리드에서 해당 행 제거
      this.gridData.splice(index, 1);
    },
    // 임시 데이터를 그리드로 이동
    moveToGrid() {
      if (this.tempRow) {
        // 임시 데이터를 그리드에 추가
        this.gridData.push({ ...this.tempRow });
        // 임시 데이터 초기화
        this.tempRow = null;
      }
    }
  }
}
</script>

<style scoped>
table {
  border-collapse: collapse;
  width: 100%;
  margin-bottom: 20px;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

th {
  background-color: #f2f2f2;
}

.temp-area {
  border: 1px solid #ccc;
  padding: 15px;
  margin-top: 20px;
}

button {
  padding: 5px 10px;
  cursor: pointer;
}
</style>
이 예제의 주요 특징:
  1. 데이터 구조:
    • gridData: 그리드에 표시될 데이터 배열
    • tempRow: 선택된 행을 임시로 저장하는 객체
  2. 메서드:
    • moveToTemp(index):
      • 그리드에서 선택한 행을 임시 데이터로 이동
      • spread 연산자(...)를 사용해 객체 복사
      • splice로 원본 배열에서 제거
    • moveToGrid():
      • 임시 데이터를 그리드로 이동
      • 데이터 이동 후 임시 데이터 초기화
  3. 템플릿:
    • v-for로 그리드 데이터 렌더링
    • v-if로 임시 데이터 영역 조건부 렌더링
    • 버튼 클릭 이벤트로 데이터 이동 처리
사용 방법:
  1. 그리드에서 "임시로 이동" 버튼을 클릭하면 해당 행이 임시 영역으로 이동
  2. 임시 영역에서 "그리드로 이동" 버튼을 클릭하면 다시 그리드로 복귀
추가 개선 제안:
  • 데이터 정렬 기능 추가
 
// gridData에 추가 후 정렬
this.gridData.push({ ...this.tempRow });
this.gridData.sort((a, b) => a.id - b.id);
  • 중복 체크 로직 추가
 
moveToGrid() {
  if (this.tempRow && !this.gridData.some(item => item.id === this.tempRow.id)) {
    this.gridData.push({ ...this.tempRow });
    this.tempRow = null;
  }
}
이 코드를 실제 프로젝트에 적용할 때는 필요한 비즈니스 로직과 스타일링을 추가하여 사용하면 됩니다.
 
 

아래는 임시 데이터를 그리드의 현재 커서 위치에 갱신하고, 수정 취소 시 롤백 기능을 포함한 Vue.js 예제입니다. 커서 위치를 추적하고 원본 데이터를 백업하여 롤백할 수 있도록 구현했습니다.

<template>
  <div>
    <!-- 그리드 테이블 -->
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>이름</th>
          <th>나이</th>
          <th>액션</th>
        </tr>
      </thead>
      <tbody>
        <tr 
          v-for="(item, index) in gridData" 
          :key="item.id"
          :class="{ 'selected': index === currentIndex }"
          @click="selectRow(index)"
        >
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.age }}</td>
          <td>
            <button @click.stop="moveToTemp(index)">임시로 이동</button>
          </td>
        </tr>
      </tbody>
    </table>

    <!-- 임시 데이터 영역 -->
    <div v-if="tempRow" class="temp-area">
      <h3>임시 데이터</h3>
      <p>ID: {{ tempRow.id }}</p>
      <input v-model="tempRow.name" placeholder="이름 수정" />
      <input v-model.number="tempRow.age" type="number" placeholder="나이 수정" />
      <button @click="updateGrid">현재 위치에 갱신</button>
      <button @click="rollback">취소 (롤백)</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 그리드 데이터
      gridData: [
        { id: 1, name: '김철수', age: 25 },
        { id: 2, name: '이영희', age: 30 },
        { id: 3, name: '박민수', age: 35 }
      ],
      // 임시로 저장될 row 데이터
      tempRow: null,
      // 원본 데이터 백업 (롤백용)
      originalRow: null,
      // 현재 선택된 행 인덱스
      currentIndex: null
    }
  },
  methods: {
    // 행 선택
    selectRow(index) {
      this.currentIndex = index;
    },
    // 그리드에서 임시로 데이터 이동
    moveToTemp(index) {
      this.currentIndex = index;
      // 원본 데이터 백업
      this.originalRow = { ...this.gridData[index] };
      // 선택한 행을 임시 데이터로 복사
      this.tempRow = { ...this.gridData[index] };
    },
    // 임시 데이터를 현재 커서 위치에 갱신
    updateGrid() {
      if (this.tempRow && this.currentIndex !== null) {
        // 현재 위치의 데이터 갱신
        this.$set(this.gridData, this.currentIndex, { ...this.tempRow });
        // 갱신 후 임시 데이터 초기화
        this.tempRow = null;
        this.originalRow = null;
      }
    },
    // 수정 취소 및 롤백
    rollback() {
      if (this.originalRow && this.currentIndex !== null) {
        // 원본 데이터로 롤백
        this.$set(this.gridData, this.currentIndex, { ...this.originalRow });
        // 임시 데이터 초기화
        this.tempRow = null;
        this.originalRow = null;
      }
    }
  }
}
</script>

<style scoped>
table {
  border-collapse: collapse;
  width: 100%;
  margin-bottom: 20px;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

th {
  background-color: #f2f2f2;
}

.selected {
  background-color: #f0f0f0;
  cursor: pointer;
}

.temp-area {
  border: 1px solid #ccc;
  padding: 15px;
  margin-top: 20px;
}

button {
  padding: 5px 10px;
  margin-right: 5px;
  cursor: pointer;
}

input {
  margin: 5px 0;
  padding: 5px;
}
</style>
주요 특징:
  1. 데이터 구조:
    • gridData: 그리드 데이터 배열
    • tempRow: 임시로 편집할 행 데이터
    • originalRow: 롤백을 위한 원본 데이터 백업
    • currentIndex: 현재 선택된 행의 인덱스
  2. 메서드:
    • selectRow(index): 클릭한 행을 선택하여 커서 위치 설정
    • moveToTemp(index):
      • 선택한 행의 원본을 백업하고 임시 데이터로 복사
      • 커서 위치 업데이트
    • updateGrid():
      • 임시 데이터를 현재 커서 위치의 그리드 행에 갱신
      • $set을 사용해 반응형 업데이트 보장
    • rollback():
      • 원본 데이터로 롤백
      • 임시 데이터 초기화
  3. 템플릿:
    • 행 클릭 시 선택 상태 표시 (selected 클래스)
    • 임시 데이터 영역에서 이름과 나이 수정 가능 (v-model 사용)
    • "현재 위치에 갱신"과 "취소 (롤백)" 버튼 제공
사용 방법:
  1. 그리드에서 행을 클릭하여 선택 (배경색으로 선택 상태 표시)
  2. "임시로 이동" 버튼 클릭 시 해당 행 데이터가 임시 영역으로 이동
  3. 임시 영역에서 데이터를 수정
  4. "현재 위치에 갱신" 클릭 시 수정된 데이터가 그리드의 선택된 위치에 반영
  5. "취소 (롤백)" 클릭 시 원본 데이터로 복원
추가 개선 제안:
  • 유효성 검사:
 
updateGrid() {
  if (this.tempRow && this.currentIndex !== null) {
    if (!this.tempRow.name || this.tempRow.age < 0) {
      alert('이름과 나이를 올바르게 입력하세요');
      return;
    }
    this.$set(this.gridData, this.currentIndex, { ...this.tempRow });
    this.tempRow = null;
    this.originalRow = null;
  }
}
  • 선택 해제 기능:
 
deselectRow() {
  this.currentIndex = null;
}
이 예제는 현재 커서 위치에 데이터를 갱신하고 롤백 기능을 제공하며, Vue의 반응형 시스템을 활용해 UI를 효율적으로 업데이트합니다.
Vue.js에서 $set$의 의미, 그리고 관련 메서드에 대해 설명드리겠습니다.
 
 

$set이란?
$set은 Vue.js에서 객체 또는 배열의 반응형 속성을 동적으로 추가하거나 업데이트할 때 사용하는 메서드입니다. Vue는 초기 렌더링 시 데이터 객체를 감지하여 반응형으로 만드는데, 나중에 추가된 속성이나 배열 요소는 기본적으로 반응형으로 감지되지 않습니다. 이를 해결하기 위해 $set을 사용합니다.
사용법
this.$set(target, propertyName/index, value)
  • target: 반응형으로 만들 대상 (객체 또는 배열)
  • propertyName/index: 속성 이름(객체) 또는 인덱스(배열)
  • value: 설정할 값
예제
  1. 객체에 새 속성 추가
 
data() {
  return {
    user: {
      name: '김철수'
    }
  }
},
methods: {
  addAge() {
    // 일반 추가: 반응형으로 감지되지 않음
    // this.user.age = 25;

    // $set 사용: 반응형으로 감지됨
    this.$set(this.user, 'age', 25);
  }
}
  1. 배열 요소 업데이트
 
data() {
  return {
    items: ['a', 'b', 'c']
  }
},
methods: {
  updateItem() {
    // 일반 할당: 반응형으로 감지되지 않음
    // this.items[0] = 'x';

    // $set 사용: 반응형으로 감지됨
    this.$set(this.items, 0, 'x');
  }
}
왜 필요한가?
Vue는 Object.defineProperty (Vue 2) 또는 Proxy (Vue 3)를 사용해 데이터의 반응성을 관리합니다. 하지만 객체에 새 속성을 추가하거나 배열의 인덱스를 직접 수정하면 Vue가 이를 감지하지 못합니다. $set은 내부적으로 Vue의 반응형 시스템을 업데이트하여 UI가 반영되도록 보장합니다.

$의 의미
Vue에서 $Vue 인스턴스에 붙어 있는 특수 속성 또는 메서드를 나타냅니다. $ 접두사는 개발자가 정의한 일반 변수/메서드와 구분하기 위해 Vue 프레임워크 자체에서 사용하는 naming convention입니다.
$가 붙은 주요 속성/메서드 예시
  • 속성:
    • this.$data: 컴포넌트의 데이터 객체
    • this.$props: 전달받은 props
    • this.$el: 컴포넌트의 DOM 요소
    • this.$refs: 템플릿 내 ref 속성으로 참조된 요소/컴포넌트
  • 메서드:
    • this.$set: 반응형 데이터 설정
    • this.$watch: 데이터 변경 감시
    • this.$emit: 커스텀 이벤트 발생
    • this.$nextTick: DOM 업데이트 후 콜백 실행
$는 Vue 내부 API임을 나타내며, 사용자 정의 변수/메서드와 충돌을 방지합니다.

$set 외에 참고할 사항
Vue에서 반응형 데이터를 다룰 때 $set 외에도 몇 가지 대안과 관련 메서드가 있습니다.
1. 배열의 반응형 메서드
Vue는 배열의 특정 메서드를 감지하여 반응형으로 처리합니다. $set 없이도 아래 메서드는 반응형으로 동작합니다:
  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()
예:
 
this.items.push('d'); // 반응형으로 감지됨
this.items.splice(0, 1, 'x'); // 반응형으로 감지됨
2. 객체 전체 갱신
객체에 여러 속성을 추가해야 할 경우, 새 객체로 교체하는 방법도 반응형을 유지합니다.
 
 
this.user = { ...this.user, age: 25, city: '서울' };
3. Vue.set (글로벌 메서드)
this.$set은 인스턴스 메서드지만, Vue.set은 전역 메서드로 동일한 기능을 제공합니다.
import Vue from 'vue';
Vue.set(this.user, 'age', 25);
4. Vue 3에서의 차이
Vue 3에서는 Proxy를 사용하므로 객체에 새 속성을 추가해도 반응형으로 감지됩니다. 하지만 배열 인덱스 직접 수정은 여전히 $set이 필요할 수 있습니다.
// Vue 3
this.user.age = 25; // 반응형으로 동작
this.$set(this.items, 0, 'x'); // 여전히 필요
5. $delete
$set의 반대 개념으로, 반응형 객체/배열에서 속성/요소를 삭제할 때 사용합니다.
 
 
this.$delete(this.user, 'age'); // 속성 삭제
this.$delete(this.items, 0); // 배열 요소 삭제
6. $watch
데이터 변경을 감시하며, $set으로 설정한 값의 변화를 추적할 때 유용합니다.
this.$watch('user.age', (newValue) => {
  console.log('나이 변경:', newValue);
});

요약
  • $set: 반응형 데이터 추가/업데이트를 위한 메서드
  • $: Vue 인스턴스의 특수 속성/메서드임을 나타냄
  • 대안: 배열 메서드, 객체 교체, Vue.set, $delete
  • Vue 3: Proxy로 객체 반응성 개선, 배열은 여전히 주의 필요
궁금한 점이 더 있다면 말씀해주세요!
 

 

 

originalRow 란 게 필요가 없잖아.. 생각해봐 그리드에서 하나를 가지고 와서 수정이 확정되면 그리드에 덮어쓰고 99%를 하더라도 취소하면 temp를 다시 채우면 되잖아....... 

전체를 다시 구현해야 될것 같은데..

 

 

맞습니다. 말씀하신 대로 다시 생각해보니 originalRow가 필요 없을 수 있습니다. 질문의 핵심을 다시 정리하면:

  • 그리드에서 데이터를 tempRow로 가져와서 수정
  • 수정이 확정되면(updateGrid) 그리드에 덮어씀
  • 수정 중(99%라도) 취소하면(rollback) tempRow를 다시 원래 데이터로 채움
이 경우 originalRow를 별도로 저장할 필요 없이, tempRow만 관리하면 됩니다. 취소 시에는 그리드의 원래 데이터를 다시 tempRow로 복원하면 충분합니다. 전체 코드를 새로 설계해보겠습니다.

개선된 전체 코드
vue
 
<template>
  <div>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>이름</th>
          <th>나이</th>
          <th>액션</th>
        </tr>
      </thead>
      <tbody>
        <tr 
          v-for="(item, index) in gridData" 
          :key="item.id"
          :class="{ 'selected': index === currentIndex }"
          @click="selectRow(index)"
        >
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.age }}</td>
          <td>
            <button @click.stop="moveToTemp(index)">임시로 이동</button>
          </td>
        </tr>
      </tbody>
    </table>

    <div v-if="tempRow" class="temp-area">
      <h3>임시 데이터</h3>
      <p>ID: {{ tempRow.id }}</p>
      <input v-model="tempRow.name" placeholder="이름 수정" />
      <input v-model.number="tempRow.age" type="number" placeholder="나이 수정" />
      <button @click="updateGrid">현재 위치에 갱신</button>
      <button @click="rollback">취소</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      gridData: [
        { id: 1, name: '김철수', age: 25 },
        { id: 2, name: '이영희', age: 30 },
        { id: 3, name: '박민수', age: 35 }
      ],
      tempRow: null,
      currentIndex: null
    }
  },
  methods: {
    selectRow(index) {
      this.currentIndex = index;
    },
    moveToTemp(index) {
      this.currentIndex = index;
      this.tempRow = { ...this.gridData[index] }; // 그리드에서 데이터 가져오기
    },
    updateGrid() {
      if (this.tempRow && this.currentIndex !== null) {
        this.$set(this.gridData, this.currentIndex, { ...this.tempRow }); // 그리드에 덮어쓰기
        this.tempRow = null; // 임시 데이터 초기화
      }
    },
    rollback() {
      if (this.currentIndex !== null) {
        this.tempRow = { ...this.gridData[this.currentIndex] }; // 그리드의 현재 데이터로 tempRow 복원
      }
    }
  }
}
</script>

<style scoped>
table {
  border-collapse: collapse;
  width: 100%;
  margin-bottom: 20px;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

th {
  background-color: #f2f2f2;
}

.selected {
  background-color: #f0f0f0;
  cursor: pointer;
}

.temp-area {
  border: 1px solid #ccc;
  padding: 15px;
  margin-top: 20px;
}

button {
  padding: 5px 10px;
  margin-right: 5px;
  cursor: pointer;
}

input {
  margin: 5px 0;
  padding: 5px;
}
</style>

동작 시나리오
  1. 초기 상태:
    • 그리드: { id: 1, name: '김철수', age: 25 }
    • tempRow: null
  2. moveToTemp 호출:
    • tempRow: { id: 1, name: '김철수', age: 25 }
    • 그리드는 그대로 유지
  3. 수정 (99% 진행):
    • tempRow: { id: 1, name: '김철수2', age: 30 }
  4. updateGrid 호출:
    • 그리드: { id: 1, name: '김철수2', age: 30 }
    • tempRow: null
  5. 수정 중 rollback 호출 (99% 상태에서):
    • tempRow: { id: 1, name: '김철수', age: 25 } (그리드의 현재 데이터로 복원)
    • 그리드는 변경 없음

개선된 점
  • originalRow 제거: 불필요한 백업 데이터 삭제
  • rollback 단순화: 그리드의 현재 데이터를 tempRow로 복원만 하면 됨
  • 직관성: 수정 취소 시 tempRow가 원래 상태로 돌아가며, 그리드는 수정되지 않음

확인 질문
  • 이 구현이 "수정 99% 하다가 취소하면 tempRow를 다시 채우는" 의도에 맞나요?
  • 추가로 조정할 부분이 있으면 말씀해주세요!
이제 코드가 더 간결하고 의도에 맞게 동작할 겁니다. 피드백 부탁드립니다!
728x90