props
를 마구잡이로 넘기며 코딩을 하고 있었다. 스스로 불길함을 느꼈고 이렇게 하다가는 어떤 컴포넌트에서 props를 사용하고 있는지 모를 것 같다는 생각이 들었다.
리액트 컴포넌트는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 단방향 구조이다.
100 depth
의 리액트 컴포넌트를 가진 웹서비스에서 가장 아래 컴포넌트가 가장 상단, 부모 컴포넌트
의 데이터가 필요하다면 무려 99단계를 거쳐서 데이터를 전달해야한다.
데이터를 전달하는 과정에서 어떤 컴포넌트는 단순히 props를 넘기는 역할만 하기도 할 것이다.
와 리액트 못쓰겠다 생각하겠지만, 똑똑한 사람들은 전역상태변수
를 좀 더 나은 방법으로 관리하기 시작했다!
어떤 똑똑한 사람이 분명 이렇게 생각했을 것이다.
'전역에 변수를 설정하고 필요한 부분에만 가져다 쓰면 안되나? 어차피 부모에서 자식으로 내리는거 원하는 곳에서 편하게 가져다 쓰자.'
이런 생각을 가지고 만들지 않았을까...
context
는 제공자
와 받는이
로 이뤄져 있습니다. 제공자가 상단에서 '필요한 사람~?을 호출하면 받는이들는 '저요~?'를 호출하면 '그래 다 가져가라' 합니다.
받는 이들은 편하게 hook을 이용해 전역상태변수를 자신의 컴포넌트에 가져올 수 있습니다.
하지만 context는 모든 상황에서 porps보다 좋은 것인지 생각해봐야합니다.
context
로 상태변수를 넘긴 컴포넌트는 완벽하게 재사용이 불가능합니다. Provider
외부에서 컴포넌트를 사용하면 원하는 값을 불러올 수 없기 때문입니다.
그래서 라이브러리를 제작할 때는 props를 통해 데이터를 자식 컴포넌트에 넘기고 있습니다.
그리고 context
를 여러개 사용할 때 하나의 값이 바뀌면 Provider로 감싸고 있는 모든 컴포넌트가 리렌더링 되기 때문에 context는 꼭 필요한 경우
에만 사용해야합니다.
코드를 통해 context를 사용하는 것과, context module을 만드는 방법을 알아보겠습니다.
//StateContext.js
import {createContext} from "react"
const ThemeContext = createContext(null)
const ThemeProvider = ({children}) => {
const store = {
state,
setState
}
return
<ThemeContext.Provider value={store}>
{children}
</ThemeContext.Provider>
}
export {ThemeContext, ThemeProvider}
굉장히 단순한 Provider 컴포넌트를 구현했습니다.
//index.js
import {ThemeProvider} from "./context/ThemeContext"
<ThemeProvider>
<App/>
</ThemeProvider>
이제 App component에서 state와 setState를 사용할 수 있게 되었다. App component의 자식 component에서 Hooks를 이용해 필요한 부분에서 다크모드를 설정할 수 있는 구조가 되었다.
프로그래밍 언어 디자인 패턴이 있듯이 리액트도 디자인 패턴이 존재하고, context와 provider도 패턴이 존재한다.
가장 basic한 방법이다. 위 다크모드를 구현하기 위한 구조로 사용했던 패턴이다.
HOC 패턴으로 provider 내부의 컴포넌트가 자유롭게 state를 사용할 수 있다.
//Provider.js
import {ThemeProvider} from "./context/ThemeContext"
<ThemeProvider>
<List/>
<Post/>
<Button/>
</ThemeProvider>
반복되는 hook 활용 메소드들을 하나로 줄여 재사용성을 높이게 하는 것.
커스텀 훅을 사용하는 이유는 기본적인 패턴은 재사용을 할 수 없기 때문입니다.
이게 어떤 의미냐면, 다크 모드가 아니라 어떤 글을 포스트 하면 게시판에 올라가고 내 저장소에 따로 올라간다고 가정해 보겠습니다.
첫 번째 패턴으로 짜게 되면,
//postboard
const postBoard = () => {
return (
<PostProvider>
<Category />
<List />
<Indicator />
</PostProvider>
)
}
//sotreBoard
const storeBoard = () => {
return (
<PostProvider>
<Category />
<List />
<Indicator />
</PostProvider>
)
}
같은 형식을 가진 코드라도 다시 작성을 해야하는 번거로움이 존재합니다.
이를 해결하기 위해 costom hooks
을 이용합니다.
재사용을 하려면 메서드는 Hooks로 선언하고, 이 Hook을 context에 할당함으로써 가능해진다.
post를 구성하는 코드가 usePost.js라고 가정해 보면,
//postContext.js
import usePost from "./usePost.js"
import {createContext} from "reat"
const initialValue= {}
export const PostContext = createContext(initialValue)
export const PostProvider = ({chidren}) => {
const postPage = usePost()
return (
<PostContext.Provider value={postPage}>
{children}
</PostContext.Porvider>
)
}
costom hook으로 정의된 postPage를 value로 사용하면서 이제는 재사용이 가능해진 context를 사용할 수 있게 되었다!
두 번째 패턴은 context와 reducer의 활용입니다.
처음 두 개념을 접했을 때 비슷한 개념이라고 생각했었는데, 공부를 할 수록 직접 써보면서 아님을 깨달았습니다.
context를 정적인 상태라 하면 reducer는 행동이라 할 수 있습니다.
reducer 패턴은 초기값과
행동 패턴에 따른 dispatch
입니다.
const ACTION_TYPES = {
add-value: "ADD-VALUE",
delete-value: "DELETE-VALUE",
put-value: "PUT-VALUE"
}
const reducer = (state, action) => {
switch(action.type){
case ACTION_TYPES.add-value: {
return [...state, action.payload]
case ACTION_TYPES.delete-value: {
return state.filter(item => item.id !== action.payload.id)
}
case ACTION_TYPES.put-value: {
const index = state.findIndex(x => x.id === action.payload.id)
state[index] = action.payload
return [...state]
}
default:
return
}
}
}
const StateProvider = ({children}) => {
const initState = []
const [state, dispatch] = useReducer(reducer, initState)
const store = {
state,
dispatch
}
return (
<StateContext value={store}>
{children}
</StateContext>
)
}
두 개념을 동시에 사용하니 코드가 좀 멋있어 진 느낌이 들었습니다. 실제로 state만 사용했을 때보다 코드의 질이 높아짐을 느꼈습니다.
개념을 어느정도 익혔으니 얼른 코딩하러..!!
참고
React Context Pattern
Hooks + Context
React Architecture: The React Provider Pattern
Provider Pattern with React Context API
custom hooks