⛳️ 인프런 - cs 지식의 정석 강의를 듣고 학습한 내용입니다.
이전 게시글(생성 패턴, 행위 패턴)에 이어서 구조 패턴을 알아보도록 하자.
객체가 어떤 대상 객체에 접근하기 전, 대리자(Proxy) 가 접근에 대한 흐름을 가로채 해당 접근을 필터링하거나 수정하도록 하는 패턴이다. 주요 목적은 원 객체에 대한 제어.
Javascript 엔진에는 Proxy가 내장되어 있어 다음과 같이 사용할 수 있다.
function createReactiveObject(target, callback) {
// Proxy 객체 생성: target 객체를 프록시로 감싸서 속성 변경 시 인터셉트
const proxy = new Proxy(target, {
// set Trap 정의: 속성이 변경될 때 실행됨
set(obj, prop, value) {
// 새로운 값(value)이 기존 값(obj[prop])과 다를 때만 실행
if (value !== obj[prop]) {
const prev = obj[prop]; // 이전 값을 저장
obj[prop] = value; // 실제 객체의 값을 업데이트
// 변경 사항을 콜백 함수로 전달
callback(`${prop}가 [${prev}] >> [${value}] 로 변경되었습니다`);
}
return true; // 프로퍼티 설정 성공
}
});
return proxy;
}
// 원본 객체 a
const a = {
"형규": "솔로"
};
// a 객체를 프록시로 감싸서 반응형 객체로 만들기: 변경 사항을 console.log로 출력하도록 설정
const b = createReactiveObject(a, console.log);
// 속성 값 변경 시도 1: 값이 동일하므로 콜백 호출되지 않음
b.형규 = "솔로";
// 속성 값 변경 시도 2: 값이 변경되므로 콜백 호출됨
b.형규 = "커플";
// 출력: 형규가 [솔로] >> [커플] 로 변경되었습니다
Proxy
에 대해 간단히 알아보고 넘어가자면,get
, set
, deleteProperty
등 객체의 동작을 가로채는 메서드를 지원한다 (위에서는 set을 사용)위 코드에서는
createReactiveObject
함수는 Proxy를 이용해 대상 객체인 target
을 감싸고, set
트랩을 적용해 속성이 변경된다면 특정 동작을 수행하도록 설정한다.set
트랩은 객체의 속성 값이 변경될 때 실행되며, 변경 사항을 콜백으로 전달한다.이전 게시글에서 다룬 패턴들은 모두 'GoF 디자인 패턴'에 기반한 전통적인 패턴들이고, Flux 패턴은 현대 개발에서 새롭게 추가된 사례이다.
Flux 패턴은 페이스북에서 '읽음 표시' 기능 장애를 해결하기 위해 고안되었다. 당시 모델과 뷰 간의 복잡한 상호작용으로 인해, 뷰에서 발생한 동작이 모델에 영향을 미치고, 모델의 변경 사항이 다시 뷰에 영향을 주는 순환 구조가 있었다. 이러한 관계는 데이터의 일관성을 유지하기 어렵게 만들었고, 이를 해결하기 위해 데이터가 '한 방향'으로만 흐르도록 설계한 Flux 패턴을 도입하게 되었다고 한다.
Redux 라이브러리는 Flux 패턴을 기반으로 만들어졌다.
Dispatcher에서 타입에 따른 행동을 명시해놓고, 이를 Store에 전달하는 코드를 확인할 수 있다.
다음은 Redux 라이브러리 내부 코드.
// 초기 상태 정의
const initialState = {
visibilityFilter: 'SHOW_ALL', // 현재 필터 설정 (기본값: 'SHOW_ALL')
todos: [] // 초기에는 비어 있는 할 일 목록
};
// 리듀서 함수: 상태(state)와 액션(action)을 받아 새로운 상태를 반환
function appReducer(state = initialState, action) {
// Action 타입에 따라 switch로 상태를 처리
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return Object.assign({}, state, {
visibilityFilter: action.filter // 필터 값을 액션에서 가져옴
});
}
case 'ADD_TODO': {
return Object.assign({}, state, {
todos: state.todos.concat({
id: action.id, // 새 할 일의 ID
text: action.text, // 새 할 일의 텍스트
completed: false // 기본값: 완료되지 않음
})
});
}
case 'TOGGLE_TODO': {
// todos 배열을 순회하며, 지정된 ID에 해당하는 항목의 완료 상태를 반전
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo; // ID가 일치하지 않으면 기존 객체 반환
}
// ID가 일치하면 완료 상태를 반전
return Object.assign({}, todo, {
completed: !todo.completed
});
})
});
}
case 'EDIT_TODO': {
// todos 배열을 순회하며, 지정된 ID에 해당하는 항목의 텍스트를 업데이트
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo; // ID가 일치하지 않으면 기존 객체 반환
}
// ID가 일치하면 텍스트를 업데이트
return Object.assign({}, todo, {
text: action.text
});
})
});
}
default:
return state; // 기존 상태를 그대로 반환
}
}