흰색 3D 유령 큐레이터가 mLog 명판이 달린 전시장의 레버를 조절하며 원본 소품 중 표시할 항목을 고르는 모습

원본 데이터는 유지하고, 현재 조건에 맞는 항목만 표시 목록으로 계산한다.

Vue 3 ref 조건을 바꿨는데 목록이 그대로일 때: computed로 조건부 항목 표시

개발 vue 2026년 11월 3일

체크박스를 누르면 isShow.value는 바뀌는데, 목록의 두 번째 항목은 처음 상태로 남아 있을 수 있다. 이때는 배열을 ref로 감쌌는지뿐 아니라 목록에 넣을 항목을 언제 계산했는지 확인해야 한다.

ref는 값을 반응형 상태로 만든다. 그러나 배열을 만들 때 실행한 filter()까지 자동으로 다시 실행하는 것은 아니다. 원본 데이터와 표시 조건으로 목록을 계속 계산하려면 computed 안에 필터를 둔다.

이 글은 Vue 3의 <script setup> 예제다. 설명을 위해 만든 작은 재현 코드이며, 실제 운영 장애를 겪었다는 기록은 아니다.

1. 원인 재현: 조건을 읽은 시점에 목록이 결정됐다

먼저 다음 코드를 보자.

import { ref } from 'vue'

const isShow = ref(false)
const rows = ref([
  { id: 'base', label: '기본 항목', optional: false },
  { id: 'extra', label: '추가 항목', optional: true }
])

// 이 줄을 실행할 때 한 번 필터링한 결과를 저장한다.
const once = ref(
  rows.value.filter(row => !row.optional || isShow.value)
)

isShow.value = true

console.log(isShow.value)                 // true
console.log(once.value.map(row => row.id)) // ['base']

once를 선언할 때 isShow.value는 false다. 필터를 통과한 것은 base뿐이다. 이후 isShow.value를 바꿔도 앞서 끝난 filter() 호출이 다시 실행되지는 않는다.

반대로 true로 시작해 두 항목을 넣었다면, 나중에 false로 바꿔도 once에는 두 항목이 남는다. 초기 조건에 따른 구성 결과를 보관한 셈이다.

여기서 “배열을 ref로 만들어도 반응성이 없다”라고 결론 내리면 안 된다. 이번 재현에서는 rows.value[0].label을 바꾸자 once가 참조하는 같은 객체의 이름도 바뀌었다. 문제가 된 것은 객체 전체의 반응성이 아니라, 조건이 바뀔 때 목록 구성도 다시 계산해야 한다는 연결이 없는 것이다.

처음 한 번 선택한 항목을 의도적으로 보관하려는 코드라면 이런 초기화가 맞을 수 있다. 사용자가 조건을 바꿀 때마다 표시 항목도 바뀌어야 하는 화면인지부터 구분하자.

2. 해결: 원본 목록과 화면에 보여줄 목록을 나눈다

다음 파일은 그대로 재현에 사용한 ConditionalList.vue다.

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

const isShow = ref(false)
const rows = ref([
  { id: 'base', label: '기본 항목', optional: false },
  { id: 'extra', label: '추가 항목', optional: true }
])

const visibleRows = computed(() =>
  rows.value.filter(row => !row.optional || isShow.value)
)
</script>

<template>
  <label>
    <input type="checkbox" v-model="isShow" />
    추가 항목 표시
  </label>
  <ul>
    <li v-for="row in visibleRows" :key="row.id">
      {{ row.label }}
    </li>
  </ul>
</template>

이 코드에서 각 값의 역할은 분명하다.

  • rows: 원본 데이터 두 항목.
  • isShow: 추가 항목의 표시 여부.
  • visibleRows: 현재 원본과 조건을 바탕으로 계산한 표시 목록.

optional: false인 기본 항목은 항상 포함한다. optional: true인 추가 항목은 isShow.value가 참일 때 포함한다. 이것은 예제의 업무 규칙이지, Vue에 내장된 optional 기능은 아니다.

Vue의 computed는 계산 중 읽은 반응형 의존성을 추적하고 결과를 캐시한다. 의존성이 바뀌면 다음에 결과가 필요할 때 갱신할 수 있다. 이 예제는 표시 조건을 getter 안에서 읽으므로, 템플릿이 사용하는 목록도 조건을 따라 바뀐다. Vue 공식 computed 안내

로컬에서 체크박스의 change 이벤트를 보내 확인한 결과는 다음과 같다.

  1. 최초 상태 false: 기본 항목만 표시.
  2. 체크한 상태 true: 기본 항목과 추가 항목 표시.
  3. 다시 해제한 상태 false: 기본 항목만 표시.
  4. 다시 체크한 상태 true: 두 항목 표시.

이 과정에서 원본 rows의 두 항목은 유지됐다. 목록에서 감추는 것과 원본에서 데이터를 삭제하는 것을 구분할 수 있다.

3. JavaScript에서는 ref 객체와 .value를 구분한다

다음 두 조건은 다르다.

const isShow = ref(false)

console.log(Boolean(isShow)) // true: ref 객체 자체
console.log(isShow.value)    // false: 객체가 담은 값

따라서 JavaScript 필터에서 row.optional ? isShow : true처럼 작성하면, 추가 항목의 조건 자리에 불리언 값 대신 객체가 들어간다. 이 예제에서는 isShow.value를 읽어야 한다.

앞의 템플릿에서 v-model="isShow", v-for="row in visibleRows"처럼 .value를 생략한 것은 템플릿의 최상위 ref 자동 언래핑 규칙 때문이다. JavaScript와 템플릿을 같은 방식으로 읽지 말자. 중첩 객체나 배열 안의 ref에는 별도 주의사항도 있으므로, 모든 위치에서 .value를 생략해도 된다는 뜻은 아니다. Vue 공식 반응성 기본

let isShow = false로 바꾸는 것도 같은 해결이 아니다. 일반 불리언의 변경 자체는 Vue가 추적하는 반응형 변경이 아니다. 이번 검증에서 일반 불리언만 읽는 computed는 처음 계산한 뒤 그 불리언을 바꿔도 이전 결과를 반환했다. 다른 반응형 의존성 때문에 우연히 재계산되는 상황과 혼동하지 않아야 한다.

4. 화면 필터로 원본을 지우지 않는다

표시 여부만 바꾸려는 상황에서 아래처럼 원본 자체를 덮어쓰면, 제외한 데이터를 나중에 다시 표시할 재료가 없어질 수 있다.

// 실제 삭제가 목적일 때와 단순 숨김이 목적일 때를 구분한다.
rows.value = rows.value.filter(row => !row.optional)

앞의 완성 예제는 원본을 유지한 채 새로운 배열을 표시 결과로 반환한다. filter()는 원본 배열에서 항목을 제거하는 연산이 아니다. 다만 내부 객체까지 깊게 복사하는 것도 아니다. 이번 검증에서도 필터 결과의 기본 항목과 원본 기본 항목은 같은 객체였다. Vue 공식 리스트 렌더링: 배열 변경과 교체

서버에서 새로운 목록을 받는 구조라면, 응답을 적절히 검증한 뒤 rows.value를 새 데이터로 바꾸고 표시 목록은 계속 computed에서 계산할 수 있다. 이번 로컬 테스트는 네트워크 요청 대신 고정된 새 배열을 대입해 이 동작을 확인했다. 실제 API 연동을 검증한 것은 아니다.

여러 데이터 원본에서 화면용 컬럼을 구성하는 큰 흐름은 PostgreSQL 동적 컬럼을 Spring Boot·Vue·IBSheet로 연결하는 글과 함께 볼 수 있다. 이 글의 필터는 일반 Vue 목록용이며, 외부 그리드에 데이터를 전달하는 API는 해당 컴포넌트의 규약을 따라야 한다.

5. v-if·key·watch가 각각 해결하는 문제를 구분한다

같은 요소에 v-for와 v-if를 놓고 v-if에서 반복 변수 row를 읽는 방식은 피하자. Vue 3에서는 같은 노드의 v-if가 먼저 평가되므로 그 반복 변수에 접근할 수 없다. 지금처럼 목록을 필터링하려면 visibleRows를 먼저 계산해 v-for에 전달하면 된다. Vue 공식 v-if와 v-for 안내

key는 목록 항목의 정체성을 Vue에 알려주는 값이다. :key="row.id"를 넣어도 처음 한 번 실행한 필터가 다시 계산되는 것은 아니다. 삭제 후 다른 행의 입력값이 남는 문제라면 Vue 동적 폼의 index key와 안정적인 ID를 별도로 확인하자.

또한 원본 데이터가 남는다고 해서 목록에서 제거된 자식 컴포넌트의 로컬 상태까지 보존되는 것은 아니다. 이번 예제는 텍스트 li이며, 입력 폼의 편집 상태 보존까지 검증하지 않았다.

watch로 조건을 감시하며 별도 배열을 수동 갱신하는 방식도 만들 수는 있다. 하지만 이 예제처럼 원본과 표시 조건만으로 결과가 정해지는 경우에는 computed 한 곳에 규칙을 두면 두 배열을 맞춰 관리할 필요가 없다. computed getter 안에서는 서버 요청이나 다른 상태 변경을 함께 실행하지 않는다. Vue 공식 computed 모범 사례

계산 결과인 visibleRows.value에 새 배열을 직접 할당하거나, 그 배열을 splice()로 편집하는 설계도 피하자. getter 전용 computed의 값 할당 제한과, 반환된 객체가 깊게 동결되는지는 서로 다른 문제다. 여기서는 결과를 읽기 전용으로 취급하고 rows 또는 isShow를 수정한다.

6. 적용 후 확인할 순서

  1. 조건의 최초 값이 false인 경우와 true인 경우를 각각 확인한다.
  2. 조건을 양방향으로 바꾸며 목록의 실제 항목을 확인한다.
  3. 원본 배열을 새 데이터로 교체해도 같은 표시 규칙이 유지되는지 확인한다.
  4. 숨겼다가 다시 표시할 때 원본 데이터가 사라지지 않았는지 확인한다.
  5. DOM을 자동 검사한다면 상태 변경 직후가 아니라 await nextTick() 이후에 확인한다.

Vue는 상태 변경 후 DOM 갱신을 묶어서 처리한다. nextTick()은 그 갱신 이후를 기다리는 용도다. 초기 필터 결과를 반응형 계산으로 바꿔 주는 함수는 아니다. Vue 공식 DOM 업데이트 타이밍

메뉴나 버튼 표시 조건에 응용하더라도 숨김 자체를 접근 권한 검사로 사용하지는 않는다. 서버 검증은 Vue disabled와 개발자 도구 조작을 다룬 글에서 별도로 설명한다.

검증 환경과 범위

2026-11-03, Node.js v24.22.1과 Vue 3.5.44에서 로컬 검증 9개를 실행해 모두 통과했다. 검증 항목은 초기 조건의 양방향 변경, 포함된 객체의 속성 변경, 원본 배열 교체, 행 조건 변경, 숨김·표시 반복, ref 객체의 참/거짓 판단, 일반 불리언의 한계, 실제 SFC 렌더링이다.

SFC 검증은 @vue/compiler-sfc 3.5.44로 위 파일을 컴파일하고 Vue 런타임을 jsdom 26.1.0에 마운트했다. 체크박스 이벤트 뒤 nextTick()을 기다려 li 내용이 1→2→1→2개로 바뀌는지 확인했다.

실제 브라우저, 업무 애플리케이션, 서버 통신, SSR, IBSheet 통합은 이번 검증 범위에 포함하지 않았다. 성능 개선 수치나 운영 장애 복구 결과도 측정하지 않았다.

재현 코드와 실제 실행 결과는 글 아래의 ZIP 파일에 담았다. 압축을 푼 폴더에서 npm ci --ignore-scripts --no-audit --no-fund와 npm test로 실행할 수 있다. 의존성 설치에는 네트워크 연결이 필요하다.

공식 근거

다음 공식 문서를 2026-11-03 확인했다.

태그

mLog

Spring Boot와 PostgreSQL로 서비스를 개발하고, Oracle Cloud에서 Docker로 mLog를 운영합니다. 쿼리가 느려지거나 컨테이너가 뜨지 않을 때 어떤 로그와 명령으로 원인을 찾았는지 기록합니다. 직접 확인한 결과와 공식 문서를 참고한 예제를 구분해 적습니다.