서문
안녕하세요. React OOP 시리즈 2번째 글을 작성합니다. 이번 글은 OOP 에서 중요한 개념인 접근제어(access control)에 대해서 알아보고, 이를 기반으로 전역상태 관리 라이브러리의 인터페이스를 분석하고, 사용 방향을 제안하려고 합니다.
OOP 에서 접근제어(access control)
우리는 왜 설계를 잘 하려고 할까요? 설계를 하는 이유는, 변경을 관리하기 위해서 입니다. 한번 쓰고 없어질 프로젝트라면, 객체지향이니, 함수형이니, 신경쓰지 않고, 동작하는 코드를 짜면 그만이겠죠. 그러나 유지보수 비용을 낮추기 위해 설계가 필요하며, 그 중 대표적인 패러다임으로 객체지향 프로그래밍이 있습니다.
OOP 에서 변경을 제어하는 대표적인 방법론이 접근 제어(access control) 입니다. 언어 레벨에서 private, public, protected 가 있습니다. 외부로 노출되어서는 안되는 요소를 private 메서드로 처리함으로써, 구현을 숨기고, 외부로 interface 만을 노출시킴으로써, 변경을 관리합니다. 이는 또 다른 원칙인 캡슐화, interface 설계 원칙, 은닉 등과 연결되지요.
private 과 public 을 사용하는 예시 코드는 다음과 같아요.
class User {
#password;
constructor(name, password) {
this.name = name;
this.#password = password;
}
// ✅ public 메서드
login(inputPassword) {
if (this.#checkPassword(inputPassword)) {
console.log(`Welcome, ${this.name}!`);
} else {
console.log("Invalid password.");
}
}
// ❌ private 메서드
#checkPassword(inputPassword) {
return this.#password === inputPassword;
}
}
JavaScript class 에서는 # 을 사용해서 private field/method 를 정의할 수 있습니다. TypeScript 에서는 private, public, protected 를 명시적으로 사용 가능합니다.
JS 에서의 접근 제어
JavaScript 는 기본적으로 class 기반 언어가 아니죠. 상태와 행동을 클래스 뿐만 아니라, 함수, 변수로 관리할 수 있습니다. 오히려 함수가 익숙하죠. 함수를 사용하면, 어떻게 접근을 제어할 수 있을까요?
2가지 방법을 알아보겠습니다. 첫 번째는 closure 를 사용한 은닉이고, 두 번째는 module system 을 활용한 은닉입니다.
closure 를 활용한 접근 제어
이는 ES6 가 나오기 전 많이 쓰이던 방법입니다. 위의 class 코드를 closure 로 바꾸면 다음과 같습니다.
function createUser(name, password) {
let _password = password;
// ❌ private 함수
function checkPassword(inputPassword) {
return _password === inputPassword;
}
return {
name,
// ✅ public 메서드
login(inputPassword) {
if (checkPassword(inputPassword)) {
console.log(`Welcome, ${name}!`);
} else {
console.log("Invalid password.");
}
}
};
}
closure 를 활용하면, 변수를 함수 안으로 은닉하고, 외부로 노출하지 않을 수 있습니다. 즉 private 과 같은 접근 제어 효과를 얻을 수 있습니다.
module system 을 활용한 접근 제어
ES6 이후로, 은닉을 위한 closure 는 직접적으로 활용되지 않는데요. 이는 ES6에 모듈 시스템이 도입되었기 때문입니다.
위 closure 코드를 module system 으로 표현하면 도움과 같아요.
const _users = new Map();
// ❌ private 함수
function checkPassword(name, inputPassword) {
return _users.get(name) === inputPassword;
}
// ✅ public 함수
export function createUser(name, password) {
_users.set(name, password);
}
// ✅ public 함수
export function login(name, inputPassword) {
if (checkPassword(name, inputPassword)) {
console.log(`Welcome, ${name}!`);
} else {
console.log("Invalid password.");
}
}
외부에 공개할 인터페이스만 export 하고, 나머지는 export 하지 않음으로써 자동으로 private, 은닉 처리합니다. React 개발자에겐 이러한 패턴이 더 익숙하겠습니다.
jotai 인터페이스 분석
지금까지 설명한 접근제어를 기반으로, 전역상태 관리 라이브러리인 jotai 와 zustand 를 분석해보겠습니다.
jotai 는 매우 간편한 인터페이스를 가집니다. atom 선언만 해주면, 어디서든 useState 와 동일한 인터페이스로 사용할 수 있는 것이 큰 장점입니다.
// atom.js
export const countAtom = atom(0);
// Counter.js
import { countAtom } from "./atom";
function Counter() {
const [count, setCount] = useAtom(countAtom);
const increaseCount = () => {
setCount((prev) => prev + 1);
};
const decreaseCount = () => {
setCount((prev) => prev + 1);
};
// ...
}
하지만, 위 방식이 객체지향 원칙으로 보면 적절하지 않습니다. “변경이 관리되지 않기 때문”입니다.
atom 을 어디서든 가져다 쓸 수 있고, 어디서든, 어떻게든 수정 가능 합니다. 0 이하의 값으로 수정해버릴 수도 있고, 1단위씩 증감시키는 제약도 걸지 못 합니다.
접근 제어를 통해 이 코드를 개선해봅시다.
클로저를 활용한 접근제어, 모듈 시스템을 활용한 접근제어, 두가지 방식으로 개선해볼게요.
module system 을 활용한 접근 제어
먼저 간단하게 모듈을 활용한 접근제어부터 알아봅니다. countAtom 자체를 export 하지 않고, increase, decrease 로직으로 캡슐화하여, 변경을 관리합니다. 더하여, 로직을 응집시켜서, 변경 사항을 추적하기 쉽다는 장점도 있습니다.
Counter.js 에서는 count 를 마음대로 수정할 수 없으며, atom.js 에서 캡슐화하여 public 으로 export 한 메서드를 호출해야만 수정 가능합니다.
// atom.js
const countAtom = atom(0);
export function useCountAtom() {
const [count, setCount] = useAtom(countAtom);
const increaseCount = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
const decreaseCount = useCallback(() => {
setCount((prev) => prev - 1);
}, []);
return {
count,
increaseCount,
decreaseCount,
};
}
// Counter.js
import { useCountAtom } from './atom';
function Counter() {
const { count, increaseCount, decreaseCount } = useCountAtom();
// ...
}
closure 를 활용한 atom 접근제어
closure 를 사용하여, atom 을 완전히 은닉할 수 있습니다.
setter 를 활용하여, 변경 범위를 제한시키고, 그 메서드만을 return 하는 함수를 만들어, 외부에서 atom 을 직접적으로 변경할 수 없도록 만듭니다.
// atom.js
function createCountAtom() {
const countAtom = atom(0);
const increaseSetAtom = atom(null, (get, set) =>
set(countAtom, get(countAtom) + 1)
);
const decreaseSetAtom = atom(null, (get, set) =>
set(countAtom, get(countAtom) - 1)
);
const countAtomValue = atom((get) => get(countAtom));
return {
countAtomValue,
increaseSetAtom,
decreaseSetAtom,
};
}
export const { countAtomValue, increaseSetAtom, decreaseSetAtom } =
createCountAtom();
// Counter.js
import { countAtomValue, increaseSetAtom, decreaseSetAtom } from "./atom";
function Counter() {
const count = useAtomValue(countAtomValue);
const increaseCount = useSetAtom(increaseSetAtom);
const decreaseCount = useSetAtom(decreaseSetAtom);
// ...
}
다만 이 코드는 보일러 플레이트가 많고, 번거로워서 DX 가 떨어져 추천하지 않습니다. 코드를 변경하는 주체는 결국 인간이기 때문에, 불필요한 추상화는 다시 변경을 어렵게 만듭니다.
지금까지 jotai 인터페이스 자체가 캡슐화를 지원하지는 않으므로, 안전하게 변경 가능하도록 하고, 로직을 응집시키기 위해서는 추가적인 노력을 해주는 편이 좋을 것입니다.
zustand 인터페이스 분석
최근에 zustand 가 무섭게 성장하고 있습니다. 여러 전역상태 라이브러리 중 download 수가 1위를 달리고 있죠.
zustand 의 인터페이스를 분석해보겠습니다.
zustand 는 기본적으로 setter 자체가 노출되지 않고, 메서드를 노출시킵니다. jotai 와 비교해서, 상태 변경을 더 잘 관리할 수 있는 형태로 설계되었습니다.
// store.js
import { create } from 'zustand';
export const useCountStore = create((set) => ({
count: 0,
increaseCount: () => set((state) => ({ count: state.count + 1 })),
decreaseCount: () => set((state) => ({ count: state.count - 1 })),
}));
// Counter.js
import { useCountStore } from './store';
function Counter() {
const { count, increaseCount, decreaseCount } = useCountStore();
// ...
}
이는 redux 를 생각나게 합니다. reducer 로 명시적인 aciton 을 정의하고, dispatch 를 통해 상태를 업데이트 하는 방식이죠.
zustand 에 reducer 와 action 을 도입하여, redux 스럽게 사용할 수도 있습니다. 사용처에서 dispatch 만을 가지고, 선언적으로 상태를 변경시킬 수 있죠.
// store.js
import { create } from 'zustand';
// action 정의
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
// reducer 함수 정의
const reducer = (state, action) => {
switch (action.type) {
case INCREASE:
return { count: state.count + 1 };
case DECREASE:
return { count: state.count - 1 };
default:
return state;
}
};
export const useCountStore = create((set) => ({
count: 0,
dispatch: (action) => set((state) => reducer(state, action)),
}));
// Counter.js
import { useCountStore } from './store';
function Counter() {
const { count, dispatch } = useCountStore();
// ...
}
마치며
오늘은 객체지향에서 중요한 접근제어 키워드를 바탕으로, 전역상태 라이브러리의 인터페이스를 분석하고, 더 나은 방향을 제안해보았습니다. 대표적인 라이브러리인 jotai 와 zustand 를 분석했고, 인터페이스 설계 측면에서는 zustand 가 더 안전하게 변경을 관리하도록 합니다. 그렇다고 하여, jotai 의 인터페이스를 비난할 수만은 없습니다. 사용처에서 별도로 로직을 응집시켜 변경을 제한시킬 수 있습니다. 라이브러리는 간편한 인터페이스를 제공하는 것만으로 역할을 다 한 것이며, 변경 관리의 책임은 개발자에게 있는 것입니다. 보다 안전하고, 유지보수 하기 좋은 코드를 짜는 데 도움이 되었으면 좋겠습니다.
'Tech > Clean Code' 카테고리의 다른 글
React OOP #1, setState props 지양하기 (9) | 2025.02.16 |
---|---|
React CleanCode #2. UI Variation에 유연하게 대응하기 (3) | 2024.01.28 |
React CleanCode #1, 합성으로 관심사를 분리하기 (12) | 2023.05.16 |