개인 과제 중 하나의 과제를 Props Drilling하게 먼저 작성하고, 완성이 되면 Context API를 활용해 전역 상태를 관리하도록 하고, 그 후엔 Redux를 활용하여 상태를 관리하도록 바꾸는 것으로 진행 하였다.
Props Drilling ➡️ Context API ➡️ Redux
하나의 그룹 아티스트를 선택하고, 그룹 멤버들에게 팬레터를 남길 수 있도록 하는 과제를 진행하였다.
필수 구현 기능은 다음과 같다.
나는 아이돌에 대해 잘 모르기에.. 드래곤볼 캐릭터들을 선정해서 진행했다.
우선 Context도, 어떤 상태 관리 라이브러리도 사용하지 않았을 경우다. 내가 사용한 state
는 모두 전역적으로 사용되기 때문에 하나의 부모 컴포넌트에서 모두 선언한 뒤 아래로 내려줄 수 밖에 없었다.
생략된 컴포넌트가 많지만 그냥 보기에도 단순히 하위 컴포넌트에서 사용하기 위해 쓸데없는 단계를 거치고 있다.
문제는 이것만이 아니다.
단순히 알람창을 띄우는 것인데 상관 없는 모든 컴포넌트가 리렌더링 된다. 왜냐면 알람을 띄우기 위한 state
가 가장 상단 컴포넌트에 위치하기 때문이다.
이를 조금이라도 개선하기 위하여 리액트가 기본적으로 제공하는 Context
를 사용해보자.
전역적으로 사용되는 state
를 크게 letter state
, modal state
, alert state
3가지로 나눌 수 있다.
따라서 우선 다음과 같이 Context
를 설정하는 파일 3개를 생성 했다.
각 파일은 다음과 같이 되어 있다.
letter-context.js
export const LetterProvider = ({children}) => {
const data = localStorage.getItem('letters');
const letterState = useState(JSON.parse(data) || initLetters);
const actions = useMemo(
() => ({
add(name, from, content) {
letterState[1](prev => {
let newLetters = [...prev];
newLetters.push(new Letter(name, from, content));
localStorage.setItem('letters', JSON.stringify(newLetters));
return newLetters;
});
},
remove(id) {
letterState[1](prev => {
let findIndex = prev.findIndex(v => v.id === id);
let newLetters = [...prev];
newLetters.splice(findIndex, 1);
localStorage.setItem('letters', JSON.stringify(newLetters));
return newLetters;
});
},
modify(id, content) {
letterState[1](prev => {
let findIndex = prev.findIndex(v => v.id === id);
let newLetter = {...prev[findIndex], ...{content: content}};
let newLetters = [...prev];
newLetters.splice(findIndex, 1, newLetter);
localStorage.setItem('letters', JSON.stringify(newLetters));
return newLetters;
});
},
}),
[],
);
return (
<LetterActionContext.Provider value={actions}>
<LetterContext.Provider value={letterState}>{children}</LetterContext.Provider>
</LetterActionContext.Provider>
);
};
export const useLetterState = () => {
const value = useContext(LetterContext);
if (value === undefined) {
throw new Error('letter context is null');
}
return value;
};
export const useLetterActions = () => {
const value = useContext(LetterActionContext);
if (value === undefined) {
throw new Error('letter action context is null');
}
return value;
};
단순히 state
를 가지는 Context
와 state
를 조작하는 action
을 담는 Context
2가지를 만들고, 하나의 Provider
에 담아서 넘길 수 있도록 하고, Provider
에서 생성한 state
를 사용할 수 있도록 custom hook을 만들었다.
이러한 작업을 통해 각 컴포넌트에서는 다음과 같이 사용할 수 있다.
Home.jsx
const Home = () => {
return (
<>
{/* 1. Z전사 나열 */}
<CharacterContainer />
{/* 2. 응원 메시지 나열 */}
<LetterProvider> // custom Provider 사용
<AllLetterContainer />
</LetterProvider>
</>
);
};
AllLetterContainer.jsx
const AllLetterContainer = () => {
const [letters] = useLetterState(); // custom hook 사용
return (
<AllLetterSection>
<LetterContainer>
{letters.map(letter => {
return <LetterRow key={letter.id} letter={letter} />;
})}
</LetterContainer>
</AllLetterSection>
);
};
따라서 Context
를 통해 다음과 같이 Props Drilling 현상을 막을 수 있게 됐다.
Props Drilling 현상은 막았지만, 여전히 문제점은 존재한다. Provider
가 내려주는 state
에 변화가 생기면 Provider
가 감싸고 있는 모든 컴포넌트가 리렌더링 된다.
따라서 여전히 알람창을 띄웠을 뿐인데 상관 없는 컴포넌트들이 리렌더링 되고 있다..
이제 마지막으로 Redux로 전환해보자.
Redux 사용을 위해 다음과 같은 폴더와 파일들을 추가 했다.
store.js
const rootReducer = combineReducers({letters, modal, customAlert});
const store = createStore(rootReducer);
export default store;
letters.js
const data = localStorage.getItem('letters');
const initialState = JSON.parse(data) || initLetters;
// action values
const ADD = 'letters/add';
const REMOVE = 'letters/remove';
const UPDATE = 'letters/update';
// action creator
export const addLetter = (name, content, from) => {
return {
type: ADD,
name: name,
content: content,
from: from,
};
};
export const removeLetter = id => {
return {
type: REMOVE,
id: id,
};
};
export const updateLetter = (id, content) => {
return {
type: UPDATE,
id: id,
content: content,
};
};
// reducer: 'state에 변화를 일으키는' 함수
// input: state와 action
const letters = (state = initialState, action) => {
let findIndex = action.id && state.findIndex(v => v.id === action.id);
let newLetters = [...state];
switch (action.type) {
case ADD:
newLetters.push(new Letter(action.name, action.from, action.content));
localStorage.setItem('letters', JSON.stringify(newLetters));
return newLetters;
case REMOVE:
newLetters.splice(findIndex, 1);
localStorage.setItem('letters', JSON.stringify(newLetters));
return newLetters;
case UPDATE:
let newLetter = {...state[findIndex], ...{content: action.content}};
newLetters.splice(findIndex, 1, newLetter);
localStorage.setItem('letters', JSON.stringify(newLetters));
return newLetters;
default:
return state;
}
};
export default letters;
Redux를 사용했더니 Props Drilling과, Proivder
로 감싸주는 태그까지 전부 사라져 훨씬 깔끔해졌다.
리렌더링도 마찬가지다.
상관 없는 컴포넌트들이 더 이상 리렌더링 되지 않는다..!!
이렇게 순차적으로 발전을 시켜보니 각각의 장단점과 사용법에 대해서 한번 더 익힐 수 있었다..!!