-
[JS] state와 mutable, immutableprograming/Language 2019. 7. 23. 18:08
안녕하세요, Einere입니다.
(ADblock을 꺼주시면 감사하겠습니다.)
해당 포스트는 Functional JS #3: State를 참고하여 정리한 포스트입니다.
State
state는 다양한 의미가 있지만, 특히 application state에 관심이 많다.
application state는 다음과 같은 의미를 지닌다.
- 모든 변수들의 현재 값(current values of all the variables)
- 모든 할당된 객체들(all allocated objects)
- 열린 파일 기술자들(open file descriptors)
- 열린 네트워크 소켓들(open network sockets)
- 기타 등등(etc)
기본적으로 위의 모든 정보는 현재 앱에서 일어나는 모든 것들을 표현하는 방식이다.
const countBiggerThanFive = numbers => { let counter = 0 for (let index = 0; index < numbers.length; index++) { if (numbers[index] > 5) { counter++ } } return counter } countBiggerThanFive([1, 2, 3, 4, 5, 6, 7, 8, 9, -5])
위 코드에서 counter는 countBiggerThanFive함수의 현재 상태를 가지고 있다. (뭔말이여)
요점은, state는 전역변수뿐 만 아니라, 지역변수로서 정의될 수 있다는 것이다.
Shared state
the more global a piece of state is, the more careful you need to be when changing it
커뮤니티를 제작한다고 가정합시다.
우리는 현재 로그인한 사람들의 목록인 currentUser라는 변수를 가지고 있습니다. 다양한 곳에서 currentUser라는 데이터가 필요하기 때문에, 해당 변수를 전역변수로 만들 수 있습니다. 이 때, currentUser는 Shared state라고 할 수 있습니다.
하지만 전역변수는 영향을 미치는 함수가 많기 때문에, 어느 함수가 currentUser에 영향을 미친다면, 이 변수를 의존하는 다른 모든 함수들도 영향을 받게 됩니다. 따라서 우리는 currentUser에 의존하는 모든 함수가 어떤 동작을 하는지 명확하게 이해해야 합니다.
이런 전역변수는 앱의 로직을 복잡하게 하며, 디버깅을 힘들게 하므로 최대한 지양하는 것이 좋습니다.
Mutable shared state
위에서 언급한 것 처럼, 가변적인 전역 변수는 mutable shared state라고 할 수 있습니다.
이러한 mutable shared state는 다음과 같은 단점을 가지고 있습니다.
It makes reasoning about code harder
일반적으로, 상태를 변경할 수 있는 영역이 넓어질수록 특정 시점의 변수 값을 예측하기 어렵습니다.
It hurts testability
함수단위 테스트를 하려면 해당 함수가 실행될 수 있는 상황을 예측해야 합니다. 그 후, 테스트 케이스를 작성하여 함수가 항상 올바르게 동작하는지 확인해야 합니다.
이것은 함수가 파라미터에만 의존하는 경우엔 쉽습니다.
하지만 mutable shared state에 의존하는 경우, 모든 테스트 케이스에 대해, 해당 상태를 예측해야 합니다.
It affects performance
만약 함수가 mutable shared state에 의존하는 경우, 그것을 동시에 실행하는 쉬운 방법이 없습니다.
왜냐하면 함수의 실행 순서에 따라 의도치 않은 동작을 하게 되거나 데드락(dead lock)이 걸릴 수 있기 때문입니다.
What we do
Use parameters instead of state
const currentUser = getCurrentUser(); const getUserBalance = () => currentUser.balance; console.log(getUserBalance());
getUserBalance함수는 전역 변수 currentUser에 의존합니다.
위 코드를 아래와 같이 수정합니다.
const currentUser = getCurrentUser(); const getUserBalance = user => user.balance; console.log(getUserBalance(currentUser));
전역 변수에 직접 의존하는 대신, 파라미터로 값을 넘겨받아, 해당 값을 의존합니다.
immutability
const getUserBalance = user => user.balance; const rewardUser = user => { user.balance = user.balance * 2 return user }; const currentUser = getCurrentUser(); console.log(getUserBalance(currentUser)); const rewardedUser = rewardUser(currentUser); console.log(getUserBalance(currentUser), getUserBalance(rewardedUser));
user.balance가 무슨 값인지는 명확하진 않지만, 어찌됫건 rewardUser는 인자로 받은 user의 balance를 수정하여 반환합니다. 이것은 currentUser와 rewardedUser 둘 다 영향을 미칩니다.
위 코드는 아래와 같이 수정하면 좋습니다.
const getUserBalance = user => user.balance; const rewardUser = user => { return { ...user, balance: user.balance * 2 }; }; const currentUser = getCurrentUser(); console.log(getUserBalance(currentUser)); const rewardedUser = rewardUser(currentUser); console.log(getUserBalance(currentUser), getUserBalance(rewardedUser));
rewardUser가 인자로 받은 user와 개별적인 새로운 객체를 만들어 반환하기 때문에, currentUser와 rewardedUser는 영향을 받지 않습니다.
이렇게 거의 항상 새로운 객체를 생성하여 반환하는 것을 immutability라고 합니다. (가끔, 인자로 받은 객체를 조작한 후 반환해야 하는 경우도 있긴 합니다.)
이런 immutable library에는 대표적으로 facebook에서 만든 immutable.js가 있습니다.
더 자세히 공부하고 싶다면, Handling State in React: Four Immutable Approaches to Consider를 참고하세요.
Pure function
순수함수는 다음과 같은 특징이 있습니다.
- they can’t depend on anything except their input (arguments)
- they have to return a single value
- they need to be deterministic (can't use random values, etc.)
위 내용은 다음과 같이 요약할 수 있습니다.
pure functions can’t access or mutate shared state.
there should be no observable effects of calling the function, other than the return value.만약 함수가 shared state를 접근, 변경하거나 다른 예측가능한 결과를 가진다면, 부작용(side effect)를 가진다고 합니다.
Side effect
부작용을 발생시키는 동작은 다음과 같습니다.
- mutating shared state or arguments
- writing to disk
- writing to console
- calling other, impure functions
- making API calls
한번 예제 코드를 봅시다.
const users = {}; // Produces side effects – mutates arguments and global state const loginUser = user => { user.loggedIn = true; users[user.id] = user; return user; }; // Produces side effects – writes data to storage const saveUserToken = token => { window.localStorage.setItem('userToken', token); }; // Produces side effects – writes to console const userDisplayName = user => { const name = `${user.firstName} ${user.lastName}`; console.log(name); return name; }; // Produces side effects – uses userDisplayName that produces side effects const greetingMessage = user => { return `Hello, ${userDisplayName(user)}`; }; // Produces side effects – makes an API call const getUserProfile = user => { return axios.get('/user', { params: { id: user.id } }); };
Referential transparency
프로그램의 동작을 변경시키지 않고, 함수 호출 표현식을 그 호출이 반환하는 값으로 대체가능한 경우, 참조 투명하다고 합니다.
다음은 참조 투명하지 않은 예제입니다.
const getUserName = user => { console.log('getting user profile!'); return `${user.firstName} ${user.lastName}`; }; const getUserData = user => { return { name: getUserName(user), address: user.address }; }; getUserData({ firstName: 'Peter', lastName: 'Pan', address: 'Neverland' });
여기서, getUserName(user)은 해당 함수가 반환하는 값으로 대체될 수 있습니다.
그러면 다음과 같이 수정할 수 있습니다.
const getUserData = user => { return { name: `${user.firstName} ${user.lastName}`, address: user.address }; }; getUserData({ firstName: 'Peter', lastName: 'Pan', address: 'Neverland' });
getUserData의 반환 객체의 name속성의 값이 getUserName(user)에서 `${user.firstName} ${user.lastName}`으로 대체되었습니다.
하지만 여전히 프로그램의 동작은 변함없이 동일합니다.
참고
https://medium.com/dailyjs/functional-js-3-state-89d8cc9ebc9e
'programing > Language' 카테고리의 다른 글
[JS] spread operator을 이용한 객체 복사 (0) 2019.07.27 [JS] prototype을 이용한 상속 (3) 2019.07.27 [JS] promise와 async await의 차이 (0) 2019.07.23 [Functional] 지연 평가와 L.map, L.filter (0) 2019.07.22 [Functional] range, take, 느긋한 L.range, L.take, L.reduce (0) 2019.07.22 댓글