리덕스의 흐름은 다음과 같다
그런데, 2번의 작업이 await-async
처럼 비동기처리라면 문제가 생긴다.
getState
로 가져올때 작업이 끝나지 않았기 때문이다.
또한 reducer
내부에서 프라미스 상태를 반환한다면...dispatch
를 await
으로 받아와하는 불상사가 생긴다.
공식문서에는 비동기를 이렇게 처리한다.
// Thunk 액션 생성자
const fetchData = () => (dispatch, getState) => {
// 비동기 작업 수행 전에 동기적인 액션을 디스패치할 수 있습니다.
dispatch(requestData());
// 예시: 비동기 작업을 수행하고 나서 결과를 받아와서 액션을 디스패치
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => dispatch(receiveData(data)));
};
// Thunk 액션 디스패치
store.dispatch(fetchData());
dispatch
에 함수를 실행하여 넘겨준다.
그러면 함수를 넘겨받은 미들웨어가 쿵짝쿵짝 처리하여 결과를 반환함.
이때 액션이 함수라면 한번 더 실행시키고, 아니라면 dispatch에 액션을 던져서 반환한다.
//thunk.js / currying기법 활용해서 고차함수를 만든다.
const thunk =({ dispatch, getState }) => (next) => (action) => {
//AsyncFunction태그도 검사해야해서 includes사용.
if (getTag(action).includes("Function")) {
return action(dispatch, getState);
}
return next(action);
};
export default thunk;
//createStore.js
//action이 함수라면 middleware로 넘겨준다.
const dispatch = (action) => {
if (getTag(action).includes("Function")) {
return middleware({ dispatch, getState })(dispatch)(action);
}
const nextState = reducer(getDeepCopy(state[reducer.name]), action);
state[reducer.name] = getDeepCopy(nextState);
observable.notify();
};
//thunk액션 생성자 함수
export const fetchDocumentsAsync = () => async (dispatch) => {
try {
const documents = await request("/documents");
dispatch({ type: FETCH_DOCUMENTS, payload: documents });
} catch (e) {
console.log(e);
}
};
//Nav코드의 일부
store.dispatch(fetchDocumentsAsync()).then((res) => console.log(res));
잘 작동한다!
여기까지는 완료다. currying개념을 이해하는데 시간이 좀 소요됐다.
그 다음은 리덕스의 상태를 구독해야한다. 이때 각 컴포넌트마다 필요한 상태가 다름.
이전 클래스형 컴포넌트에서는 connect
라는 기능을 사용했었다.
지금까지 이전 기능을 많이 만들었으니 비교적 최신(?)기술인 useSelector
를 넣어보겠따.
const yourReduxState = useSelector((state) => state.yourReduxState);
render
메서드를 넘겨주면 될것 같다.let observable = null;
const useSelector = (func, callback) => {
const selectedState = func(state);
console.log(selectedState);
observable = Object.freeze(new Observable(selectedState));
observable.subscribe(callback);
return selectedState;
};
밖에서 선언했던 observable
을 null
로 만들고 다시 할당하는 방식으로 바꾸었다.
이렇게 만든 useSelector
를 Nav컴포넌트 렌더 내부에서 사용한다.
밖에서 사용할 시 바뀐 상태를 가져올 수 없기 때문이다.
참고로 함수 컴포넌트는 함수 자체를 새로 실행하기에 상관없다.
this.render = () => {
const data = store.useSelector(
(state) => state.documentsReducer,
this.render
);
$nav.innerHTML = "";
new DocumentListHeader({ $target: $nav });
documentList = new DocumentList({
$target: $nav,
initialState: data.documents,
createDocument,
removeDocument,
});
....
이제 Nav컴포넌트를 Component클래스를 상속받아 사용해보자
useSelector로 this.render
를 보낼때 문제가 생겼다.
this.render
를 useSelector에 콜백으로 전달해서, this바인딩 문제가 생김.
간단하게 화살표 함수로 구현하려 했는데...this.render()
가 실행은 되는데 렌더링되지 않았다.
콜백이 Observable
클래스로 넘어가니 this
에 undefined
가 바인딩 되었음.
결국 this.render.bind(this)
이렇게 강제로 바인딩하니 됨.
render() {
const data = store.useSelector(
(state) => state.documentsReducer,
this.render.bind(this)
);
아마 useSelector
에서 함수째로 전달하는 과정에서 문제가 생긴게 아닐까 싶다.
한번 고쳐보겠음
const useSelector = (func, callback) => {
const selectedState = func(state);
observable = Object.freeze(new Observable(selectedState));
observable.subscribe(()=>callback);
return selectedState;
};
그래도 안되네...일단 넘어가자.
분명 알기로 화살표 함수의 this
는 선언 시점의 상위스코프렸다...
프롭스를 전달할때 문제가생김. 기본이 되는 Component클래스에선 render
를 이미 인스턴스 생성시점에 실행해버림. 따라서 프롭스로 데이터를 전달하게되면, 렌더 된 이후에 프롭스가 전달됨.
=> 첫 렌더 시점시 프롭스들이 undefined나옴
따라서 기본Component클래스를 수정할 필요가 있다.
export default class Component {
state;
props;
constructor({ $target, tagName, props }) {
this.$target = $target;
this.wrapper = tagName ? document.createElement(tagName) : null;
this.props = props;
this.state = props?.initialState;
this.wrapper && this.$target.appendChild(this.wrapper);
this.setEvent();
this.render();
}
이렇게 수정완료! 점점 길어지는건 기분탓인데...
코드 수정중 라우팅시 화면을 지우지 않게되어 에디터 페이지가 2개씩 보이는 현상 발생
라우팅에서 처리해주는 것이 나을것 같음. 하지만 어떻게 처리할까?
생각해본 방법은
fragment
에 라우트가 필요한 컴포넌트들을 붙인다.fragment
내부를 싹 비움.이렇게 해도 Editor내부 구조가 좀 복잡해서 2개가 로딩됐다.
일단 다른 페이지가 생겼을때 처리가 가능하니, 처리한건 남겨두고 다음 파트로 고고싱
생각보다 render
시점 이전에 일어나야하는 태스크들이 많다.
따라서 이전에 준비할 수 있게 Component클래스에 추가해주었다.
constructor({ $target, tagName, props }) {
this.$target = $target;
this.wrapper = tagName ? document.createElement(tagName) : null;
this.props = props;
this.state = props?.initialState;
this.wrapper && this.$target.appendChild(this.wrapper);
this.setEvent();
this.prepare();
this.render();
}
점점 뭐가 많아지는건 기분탓이다..
문서가 4개있고, 맨 마지막 문서를 클릭했을 때
맨 위의 문서를 클릭했을때
왜 이런결과가 발생할까?
바로 이벤트 버블링때문!
setEvent() {
this.addEvent("click", ".document-item-inner", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
}
if (!e.target.closest("button")) {
e.stopPropagation();
console.log("hi");
push(
`/documents/${this.wrapper.dataset.id}`,
this.props.highlightSelectedDocument
);
}
});
}
이렇게 e.stopPropagation()으로 버블링 전파를 막아주면 해결된다.
버블링은 참고로 자식 => 부모순으로 이벤트가 전파되는걸 의미함.
따라서 재귀적 렌더링된 컴포넌트들은 모두 클릭 이벤트 감지기능이 있어서, 클릭 이벤트핸들러가 동작했다.
해결!
이것도 역시 해결했던 일. Component클래스를 상속받아 사용하느라 일관성을 지키려고 appendChild
를 사용했다. 덕분에 부모노드의 innerHTML
를 비워줘야하는 사태가 발생했고...(중략)
한두시간 헤메다 그냥 replaceChildren
을 사용하기로 합의.
시간이 너무 낭비됐다..
이건 이전에도 해결해봤다. input value를 상태와 분리해서 관리하는 거다.
일단 DocumentPage
부터 클래스로 리팩토링 해보자..