Robert C. Martin의 클린 소프트웨어에서는 소프트웨어의 세 가지 목적에 대해 얘기한다.
- 실행 중에 오류 없이 제대로 동작해야 한다.
- 소프트웨어는 변경을 위해 존재한다.
- 코드를 읽는 사람과 의사소통해야 한다.
이것이 바로 모듈이 존재해야 하는 이유라고 한다. 그래서 우리는 모듈을 지원해 주는 Vue.js라는 프레임워크를 사용하고 있다. (front end 분야에서는 컴포넌트가 이러한 역할을 한다.) 그런데 왜 우리가 보고 있는 그 코드는, 무엇을 의미하는지 알 수 없을까? 단지 모듈의 사용만으로 바로 좋은 코드가 만들어지지 않기 때문이다.
우리의 적은 바로 "소프트웨어의 정체성을 잃어버린 코드"다. 이런 코드에게 정체성을 주는 건, 쉽지 않은 여정임을 알고 있다. 하지만 "천 리 길도 한 걸음부터"라는 속담이 있듯이, 조금씩 걸음을 내딛다 보면 좋은 코드와 만나지 않을까?
Vue.js로 짠 코드들 중 가장 많이 말썽을 부리며 예상치 못한 동작을 발생시키며, 코드 파악에 어려움을 주는 기능이 있다. 알만한 사람들은 아마 다 알 것이다. 바로 watch이다.
Vue.js는 다양한 기능들을 제공한다. watch 속성도 그중 하나다. watch는 데이터 변경을 관찰하고 이에 반응하는 속성이다. 기능을 제공한다는 것 자체가 필요가 있다는 말인데, 이런 문제가 발생한다는 건 사용을 잘못하고 있다는 반증이다. 바로 watch를 잘못 만들어진 기능을 메꾸기 위한 용도로 사용하기 때문이다. watch가 있는 이유는 데이터의 흐름이 양방향 바인딩이기 때문에, watch를 통해 감시하지 않으면 변화를 감지하기 어려운 상황들이 있기 때문이다.
그런데 watch와 결합해, Vue.js에서 함께 문제를 일으키는 특성이 있다. 바로 Vue.js의 데이터 변경 방식이다. Vue.js는 data 속성 등 다양한 방식으로 변경 내용을 추적한다. 속성의 추가 제거와 같이 변경 감지가 불가능한 요소들이 있지만, 실제 이 동작에 대한 코드 작성은 가능하다. 보통 이렇게 변경/감지가 제대로 일어나지 않는 현상이 발생할 때, 동작에 이해가 명확하지 않은 누군가가 watch를 추가하는 불상사를 만든다. 특히 deep, immediate과 같은 속성을 추가하고, 호출하는 함수 동작 안에 다시금 해당 값을 변경하는 코드라도 있으면, 개발자는 이미 이 코드의 제어권을 잃었다 해도 무방할 정도다.
React에는 setState라는 함수가 있다. React에 setState라는 함수가 있어서, 이 함수를 통해서만 state가 변경이 가능하다. state를 변경하기 까지의 제어 흐름이 일관적으로 이어질 수 있다. 잘못 구성된 Vue.js와 비교를 해볼까?
<script>
export default {
data() {
return {
testVal: {}
}
},
created () {
this.init()
},
methods: {
init() {
this.setA()
this.setB()
},
setA() {
this.testVal.a = "a" // 이거 가능
this.setC()
},
setB() {
this.testVal.b = "b" // 이거 가능
},
setC() {
this.testVal.c = "c" // 이거 가능
}
}
}
</script>
결국 testVal의 a,b,c 속성에 값을 대입해 줘야 하는 상황인데, 코드가 여기저기 분리되어 있다.
init() {
const tempVal = this.setTest()
this.setState({ testVal: tempVal})
},
setTest() {
return { a: "a", b: "b", c: "c" }
}
React로 하면 setState라는 함수를 사용해야 하기 때문에, 변경되는 값을 한 번에 처리하게 된다. (사실, react 다 까먹음.. ㅠ.ㅠ) 그리고 state의 특정 값 자체를 대체해버리기 때문에, observer에서 변화감지가 확실하게 된다.
그러면 Vue.js에서 어떻게 해야 할까? 우선 목적과 구현을 분리해야 한다. 실제 데이터 조작과 관련된 함수들은 특정 함수(대체로 초기화 함수)에게 책임을 가져가고, 가공이 필요한 값은는 가공과 관련된 함수에게 위임하는 것이다. 여기서 중요한 건, 그리고 초기화 데이터의 구조가 명확해야 하고, 가공과 관련된 함수는 side effect가 없어야 한다는 것이다. (가공된 값을 그대로 리턴) 그래야만 제어 흐름이 일정해질 수 있다. side effect에 관한 내용은 함수형 프로그래밍이나 side effect/순수함수/부수효과 키워드로 검색하면 많이 나온다.
- 초기화 데이터의 구조가 명확해야 한다.
- 데이터 조작과 관련된 함수와 가공하는 함수는 분리한다.
- 가공과 관련된 함수는 side effect가 없어야 한다.
아까의 코드를 그럼 간단히 바꿔보자.
<script>
export default {
data() {
return {
testVal: {
a: null,
b: null,
c: null
}
}
},
created () {
this.init()
},
methods: {
init() {
this.testVal = { a: this.getA(), b: this.getB(), c: this.getC() }
// 변수나 상수에 대입도 가능
},
getA() {
return "a"
},
getB() {
return "b"
},
getC() {
return "c"
}
}
}
</script>
대략 이런 형태로 나올 것이다.
예시나 설명이 적절한지 잘 모르겠지만, 우선은 내가 사용하는 코드들에 값을 직접 변경하는 부분들이 흩어져있는지 확인해보고 값을 return 하는 형태로 변경을 해보자. 그게 너무 어려우면 가능한 부분만 함수로 분리하는 방법도 있다. 그러면 자연스럽게 깔끔해지고 알아보기 쉬워지는 코드를 경험할 것이다. 일단 해보자!!!
다음 글에는 함수와 관련해서, Vue.js의 지원 기능 하나에 대해 얘기해보려 한다. 부족한 글을 읽어주셔서 감사합니다. ^_^
제가 그 watch지옥에 빠져있씁니다...ㅠㅠ