Vue.js Composition API hooks+

강정우·2023년 5월 6일
0

vue.js

목록 보기
58/72
post-thumbnail

props

  • 만약 기존의 코드를 migration 할 때 props는 어떻게 처리할까?

  • 물론 props를 import해온다고 해도 사용할 수 없다. 그렇다면 방법이 없을까? => X

  • 바로 앞서 언급한 "인수"를 활용하면 된다.

import {computed, ref, watch} from "vue";

const useSearch = (props) => {
    const enteredSearchTerm = ref('');
    const activeSearchTerm = ref('');
    const availableItems = computed(()=>{
        let users = [];
        if (activeSearchTerm.value) {
            users = props.items.filter(props.filterFn);
        } else if (props.items) {
            users = props.items;
        }
        return users;
    })
    watch(enteredSearchTerm,(newValue)=>{
        setTimeout(() => {
            if (newValue === enteredSearchTerm.value) {
                activeSearchTerm.value = newValue;
            }
        }, 300);
    })
    const updateSearch = (val) => {
        enteredSearchTerm.value = val
    }
}
export default useSearch;
  • 이때 코드의 유연성을 위해 props.filterFn을 적어놓았는데 이렇게 해도되고 혹은
const useSearch = (props) => {
    const enteredSearchTerm = ref('');
    const activeSearchTerm = ref('');
    const availableItems = computed(()=>{
        let users = [];
        if (activeSearchTerm.value) {
            users = props.items.filter((item) =>
                item[props.searchProp].includes(activeSearchTerm.value)
            );
        } else if (props.items) {
            users = props.items;
        }
        return users;
    })
    watch(enteredSearchTerm,(newValue)=>{
        setTimeout(() => {
            if (newValue === enteredSearchTerm.value) {
                activeSearchTerm.value = newValue;
            }
        }, 300);
    })
    const updateSearch = (val) => {
        enteredSearchTerm.value = val
    }
    return {enteredSearchTerm, availableItems, updateSearch}
}
  • 이렇게 해도 된다.
  • 이때 앞서 언급했듯 넘어오는 값이 computed라면 readOnly라는 것을 잊지 말아야한다.

사용될 컴포넌트

const props = defineProps({
  users : {
    type: Object,
    default : ()=>({id:String, fullName:String, projects:[]})
  }
})

const enteredSearchTerm = ref('');
const activeSearchTerm = ref('');
const availableUsers = computed(()=>{
  let users = [];
  if (activeSearchTerm.value) {
    users = props.users.filter((usr) =>
        usr.fullName.includes(activeSearchTerm.value)
    );
  } else if (props.users) {
    users = props.users;
  }
  return users;
})
watch(enteredSearchTerm,(newValue)=>{
  setTimeout(() => {
    if (newValue === enteredSearchTerm.value) {
      activeSearchTerm.value = newValue;
    }
  }, 300);
})
const updateSearch = (val) => {
  enteredSearchTerm.value = val
}

const sorting = ref(null);
const displayedUsers = computed(() =>{
  if (!sorting.value) {
    return availableUsers.value
  }
  return availableUsers.value.slice().sort((u1, u2)=>{
    if (sorting.value === 'asc' && u1.fullName > u2.fullName) {
      return 1;
    } else if (sorting.value === 'asc') {
      return -1;
    } else if (sorting.value === 'desc' && u1.fullName > u2.fullName) {
      return -1;
    } else {
      return 1;
    }
  })
})
const sort = (mode) => {
  sorting.value = mode
}
  • 원래 위와 같은 식을 훅을 통하면
const props = defineProps({
  users : {
    type: Object,
    default : ()=>({id:String, fullName:String, projects:[]})
  }
})
const {enteredSearchTerm, availableItems:availableUsers , updateSearch} = useSearch({items:props.users, searchProp:'fullName'})

const sorting = ref(null);
const displayedUsers = computed(() =>{
  if (!sorting.value) {
    return availableUsers.value
  }
  return availableUsers.value.slice().sort((u1, u2)=>{
    if (sorting.value === 'asc' && u1.fullName > u2.fullName) {
      return 1;
    } else if (sorting.value === 'asc') {
      return -1;
    } else if (sorting.value === 'desc' && u1.fullName > u2.fullName) {
      return -1;
    } else {
      return 1;
    }
  })
})
const sort = (mode) => {
  sorting.value = mode
}
  • 매우 간결하게 줄일 수 있다.

커스텀 훅 잘 못된 코딩

const props = defineProps({
  user:{
    type: Object,
    default: ()=>({projects:String, fullName:String})
  }
})

const {enteredSearchTerm, availableItems, updateSearch} = useSearch({items:props.user.projects, searchProp:'title'});

const hasProjects = computed(() => {
  return props.user.projects && availableItems.value.length > 0
})

watch(props,()=>{
  enteredSearchTerm.value = ''
})
  • 만약 이제 위와같은 useSearch라는 커스텀훅을 사용하여 props의 데이터를 넘겨줘야하는 상황이라 가정해보자 그럼 다음과같은 결과를 받아볼 수 있다.

  • 바로 null이다. 그 이유는 초기에 선택한 사용자(user)가 없다는 것이다.
    그래서 초기 사용자가 null이 되어버리는데 null 값의 프로젝트에 액세스하려니 문제가 발생하는 것이다.
    그렇다면 해결법이 무엇이 있을까?

1. 변수나 상수로 새로운 helper 생성

const props = defineProps({
  user:{
    type: Object,
    default: ()=>({projects:String, fullName:String})
  }
})

const projects = props.user ? props.user.projects : [];					<=여기

const {enteredSearchTerm, availableItems, updateSearch} = useSearch({items:projects, searchProp:'title'});

const hasProjects = computed(() => {
  return props.user.projects && availableItems.value.length > 0
})

watch(props,()=>{
  enteredSearchTerm.value = ''
})
  • 하지만 여기서 끝! 이 아니다. 앞서 언급했듯 props.user까지는 reactive하지만 내부 프로퍼티로 들어가면 그 값은 더이상 ref한 값이 아니라고 하였다. 따라서 setup 함수가 최초로 실행됐을 때 projects 값이나 빈 배열을 가져오도록(pull) 설정했는데 그러면 끝! 즉, null이 되고 더 이상 값이 바뀌지 않는다는 것이다.

  • 그래서 바뀌는 사용자에 반응하여 프로젝트를 업데이트해야하는데 가장 간단한 방법으로는 computed 메서드이다.

const projects = computed(()=>{
  return props.user ? props.user.projects : []
});

const {enteredSearchTerm, availableItems, updateSearch} = useSearch({items:projects, searchProp:'title'});
  • 하지만 역시 computed만 사용한다면 계산은 되겠지만 마크업에 반영은 안 된다. 따라서 toRef로 변환 가능한 값을 만들어준 후 템플릿에 던져줘야한다.
const props = defineProps({
  user:{
    type: Object,
    default: ()=>({projects:String, fullName:String})
  }
})
const {user} = toRefs(props);

const projects = computed(()=>{
  return user.value ? user.value.projects : []
});

const {enteredSearchTerm, availableItems, updateSearch} = useSearch({items:projects, searchProp:'title'});
  • 그런데 자세히 보면 useSearch에 넘기는 값인 projects는 ref한 값이고 이 ref한 값에 접근하려면 반드시 .value가 있어야한다고 했다.

  • 하지만 내가 작성해놓은 코드를 보면 그렇지 않다는 것을 확인할 수 있다. 따라서 해당 훅에서 props에 해당하는 값들에 .value를 붙여줘야한다.

const useSearch = (props) => {
    const enteredSearchTerm = ref('');
    const activeSearchTerm = ref('');
    const availableItems = computed(()=>{
        let users = [];
        if (activeSearchTerm.value) {
            users = props.items.value.filter((item) =>
                item[props.searchProp].includes(activeSearchTerm.value)
            );
        } else if (props.items.value) {
            users = props.items.value;
        }
        return users;
    })
    watch(enteredSearchTerm,(newValue)=>{
        setTimeout(() => {
            if (newValue === enteredSearchTerm.value) {
                activeSearchTerm.value = newValue;
            }
        }, 300);
    })
    const updateSearch = (val) => {
        enteredSearchTerm.value = val
    }
    return {enteredSearchTerm, availableItems, updateSearch}
}
  • 그리고 만약에 위와같이 만들어줬다면 위 useSearch 훅을 사용하는 모든 컴포넌트는 반드시 ref가 되어야한다.

  • 즉, 결국 컴포넌트단에서 넘겨주는 모든 인수 값이 전부 ref해야지 좋다. 그래야만 데이터가 변함에 따라 로직이 실행되고 이는 바로 재실행되기 때문이다.

const props = defineProps({
  users : {
    type: Object,
    default : ()=>({id:String, fullName:String, projects:[]})
  }
})
const {enteredSearchTerm, availableItems:availableUsers , updateSearch} = useSearch({items:toRefs(props.users), searchProp:'fullName'})			=> 잘못된예

const {users} = toRefs(props);
const {enteredSearchTerm, availableItems:availableUsers , updateSearch} = useSearch({items:users, searchProp:'fullName'})			=> 잘된예

const sorting = ref(null);
const displayedUsers = computed(() =>{
  if (!sorting.value) {
    return availableUsers.value
  }
  return availableUsers.value.slice().sort((u1, u2)=>{
    if (sorting.value === 'asc' && u1.fullName > u2.fullName) {
      return 1;
    } else if (sorting.value === 'asc') {
      return -1;
    } else if (sorting.value === 'desc' && u1.fullName > u2.fullName) {
      return -1;
    } else {
      return 1;
    }
  })
})

커스텀 훅 잘 못된 코딩2

const {enteredSearchTerm, availableItems, updateSearch} = useSearch({items:projects, searchProp:'title'});

const hasProjects = computed(() => {
  return user.value.projects && availableItems.value.length > 0
})

watch(props,()=>{
  enteredSearchTerm.value = ''
})
  • 위와같은 코드는 지양해야한다.
    무슨 말이냐 => useSeach같이 커스텀훅에서 넘어온 값을 사용중이 컴포넌트에서 임의로 바꾸면 안 된다는 것이다.

  • 이유는 앞서 주구장창 말했던 코드를 이해하고 유지하기 쉽게 만들어야 하기 때문이다.

  1. enteredSearchTerm 코드를 보면 이 컴포넌트의 ref라고 생각할 수 있지만 상단에선 찾을 수 없다.
  2. 훅 밖에서 enteredSearchTerm을 바꾸면 훅 안에 적용한 로직과 충돌이 생길 수 있다.

마치 vuex의 commit, dispatch 처럼

  • 그래서 나온것이 별도로 값을 변경하는 함수를 선언하는 것이다.

  • 다행히 우리는 useSearch에서 updateSeach라는 함수를 가져오기때문에

const {enteredSearchTerm, availableItems, updateSearch} = useSearch({items:projects, searchProp:'title'});

const hasProjects = computed(() => {
  return user.value.projects && availableItems.value.length > 0
})

watch(props,()=>{
  updateSearch('')
})
  • 위와같이 설정해주면 좋다.

많은 부분을 generic하게 만들기

  • 커스텀 훅은 굉장히 일반적이여야한다. 따라서 많은 부분을 동적으로 값을 취할 수 있도록 하면 좋다. 아래의 예시를 봐보자.
const useSort = (props) => {
    const sorting = ref(null);
    const displayedUsers = computed(() =>{
        if (!sorting.value) {
            return props.availableUsers.value
        }
        return props.availableUsers.value.slice().sort((u1, u2)=>{
            if (sorting.value === 'asc' && u1.fullName > u2.fullName) {
                return 1;
            } else if (sorting.value === 'asc') {
                return -1;
            } else if (sorting.value === 'desc' && u1.fullName > u2.fullName) {
                return -1;
            } else {
                return 1;
            }
        })
    })
    const sort = (mode) => {
        sorting.value = mode
    }
    return{ 
        displayedUsers, sort
    }
}
  • 위코드는 props로 값을 받아와서 받아온 값을 나열하고 이를 정렬해주는 코드이다. 이때 props로 넘겨받은 값에 fullName이라는 속성이 없다면 이는 굉장히 generic하지 못 하다고 할 수 있다. 따라서 아래와같이 바꿔주면 좋다.
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글