해당 글은 리액트 공식문서인 Scaling Up with Reducer and Context 를 토대로 하여 새롭게 공부한 내용입니다.
공식문서에서는 ToDoList
를 새롭게 만들어 설명 하지만
나는 이미 만들어둔 적이 있으니 이미 만든 것을 리팩토링 하는 과정으로 해보자
UI
스케일 확장에 있어 UseContext
의 필요성이전 useReducer
를 공부한 이후 리액트로 ToDoList 만들기 (useState , useReducer) 를 통해 ToDoList
를 만들어보았다.
전체적인 컴포넌트 구조를 모두 지금 보여주지는 않겠지만 최상단 컴포넌트인 App
컴포넌트를 살펴보면 다음과 같다.
Use Context 를 사용해야 하는 이유 에서 Props Drilling
을 최대한 지양해야 한다고 하였지만
현재 컴포넌트의 구조는 App
에서 정의된 이벤트 핸들러들과 state, dispatch
등이 하위 컴포넌트로 전달되고 있는 모습을 볼 수 있다.
물론 위 게시글에서 해당 예시가 좋을 수도 있다고 이야기 하였지만
상위에 존재하는state, dispatch
등을 효과적으로 하위 컴포넌트에 전달하는 또 다른 방식을
공식 문서에서 제시하고 있기 때문에 공부해보자
현재의 컴포넌트는 하위 컴포넌트가 많아봐야 2개 남짓이지만
만약 나중 UI
의 스케일이 커져 컴포넌트의 계층 구조가 깊어지면 깊어질 수록
Drilling
해야 하는 Props
의 수가 그에 따라 늘어나게 될 것이다.
이는 props
를 전달해야 하기 위해 인수로 건내주고, 받은 props 를 하위 컴포넌트로 전달하는 과정의 반복을 유발 할 것이다.
상위 컴포넌트에서 하위 컴포넌트에게 state , dispatch
를 Props Drilling
을 효과적으로 해결하기 위해 useContext , useReducer
를 이용하여 리팩토링 해보자
처음 토이프로젝트를 만들 때에는 파일 디렉토리 구조를 짜기 귀찮아서 그냥 한 페이지에 우다다 넣어버렸는데
사실은 이렇게 하면 안된다.
이는 컴포넌트, 모듈 기반 UI 구성에 맞지 않는 방법이다.
컴포넌트들을 pure
하게 관리하기 위해서 각자 독립적인 파일에 관리함으로서
코드의 가독성과 유지 보수성을 높이기 위해 파일 하나에 한 컴포넌트나 관련된 컴포넌트 몇 가지만 같이 넣어두는 것이 모듈화이다.
위 예시처럼 한 페이지에 모든 컴포넌트를 구성하여 한 컴포넌트를 export
하는 행위는 모듈화에 맞지 않는다.
컴포넌트가 다른 컴포넌트가 필요 없이 스스로 존재 할 수 있는 최하위 컴포넌트도 있지만
여러 컴포넌트의 조합으로 만들어지는 컴포넌트도 존재한다.
이러한 컴포넌트는 조합에 사용되는 재료 컴포넌트들과 의존성을 가지고 있다.
재료 컴포넌트라는 말은 실제로 존재하는 말이 아니다. 그냥 내가 이해하기 위해 쓴 개념 키킥
하지만 파일 별로 모듈화 하여 관리 할 때 의존하고 있는 재료 컴포넌트들을 import
하는 행위를 통해
해당 컴포넌트와 의존성있는 컴포넌트를 명시적으로 관리 할 수 있다.
팀 작업 시 각자가 담당한 컴포넌트를 파일 단위로 분리하면 각자의 역할에 따라 코드를 분리하여 작업 할 수 있다.
이는 현업을 원활하게 하고 코드 충돌을 최소화 하는데 도움이 된다.
파일 디렉토리를 다음처럼 Component , core
폴더를 기준으로 나눠주었다.
App.js
./Component/Button.js
./Component/Input.js
./Component/TodoInput.js
./Component/ToDoText.js
./Component/TodoList.js
./core/TaskReducer.js
다음처럼 모듈화하여 나눠주었다.
이렇게하여 나눠주니 확실히 각 컴포넌트 별 의존성을 알 수 있게 되었다.
캡쳐하다보니 수정해야 할 로직이 몇 가지 보이긴 하는데 우선 지금은 useContext , useReducer
를 효과적으로 이용하는 것에 집중하여 리팩토링 해보도록 하겠다.
현재의 문제점을 뽑자면 App
컴포넌트가 어떤 컴포넌트인지 한 눈에 알아보기 힘들다는 것일 것이다.
App
컴포넌트의 역할이 렌더링 하는 것뿐이 아니라 하위 컴포넌트들에게 필요한 메소드를 생성하고 Props
를 건내주는 것 까지
세 가지 기능을 하고 있다는 것이다.
차라리 상위 컴포넌트에서는 dispatch
메소드만 하위 컴포넌트들로 전달해주고 하위 컴포넌트에서 dispatchAdd ... dispatchRemove
메소드를 정의하여 사용하도록 하는 것이
컴포넌트의 기능들을 이해하기 훨씬 쉬울 것이다.
그것에 맞춰 수정해보자
현재의 가장 큰 문제는 App
컴포넌트가 반환하는 모든 컴포넌트들은
App
컴포넌트에서 정의된 state , setter function
등을 props
로 받고 있기 때문에
App
컴포넌트에게 강하게 의존하고 있다는 것이다.
또한 App
컴포넌트에서 하위 컴포넌트의 기능들을 모두 정의해두었기 때문에 App
컴포넌트의 기능을 확실하게 파악하기 힘들다.
컴포넌트의 역할을 확실히 하기 위해 컴포넌트를 더욱 독립적인 단위로 쪼개고
각 컴포넌트의 역할이 명확히 되도록 수정해보자
App.js
이전과 다르게 App
컴포넌트는 Todoheader , TodoList
두 컴포넌트의 조합으로 이뤄진 컴포넌트로 수정해주었다.
이를 통해 App
컴포넌트의 역할이 무엇인지 한 눈에 파악하기 편해졌다.
또한 App
컴포넌트는 하위 컴포넌트의 로직들을 정의하지 않고, 단순히 현재의 state
인 tasks
와 dispatch
만을 생성하여 props
로 내려주고 있기 때문에
역할을 명확히 알아보기 쉽다.
./Component/ToDoHeader.js
컴포넌트의 조합으로 만들기 위해 App
컴포넌트가 반환하던 내용을 컴포넌트화하여 만들어주었다.
해당 부분의 모습은 다음과 같이 생겼다.
이후는 수정이 일어난 부분들에 대해서만 첨부하도록 하겠다.
./Component/Input.js
./Component/TodoInput.js
./Component/TodoText.js
./Component/TodoList.js
모듈화를 모두 하고 컴포넌트를 퓨어하게 만드니 각 컴포넌트의 역할을 명확하게 알기 쉬워졌다.
useContext
를 이용하기컴포넌트를 퓨어하게 만들었다고 하더라도 여전히 App
컴포넌트의 반환값은
렌더링과 하위 컴포넌들에게 props
를 건내주는 모습은 동일하다.
이뿐만 아니라 TodoList
컴포넌트의 경우에는
여전히 TodoInput
과 TodoText
부분에 props
들을 전달하고 있는 모습을 볼 수 있다.
이러한 문제를 해결하기 위해 useContext
를 이용해보자
./Core/TaskContext.js
Context
들만을 담은 파일을 모듈화 하여 다른 폴더에서 생성해준다.
App.js
위에서 선언한 TaskContext.js
파일에서 Context
들을 가지고와 App
컴포넌트에서 Context
로
하위 컴포넌트들에게 tasks , dispatch
를 건내주도록 하였다.
이를 통해 하위 컴포넌트들이 props
로 받던 tasks , dispatch
등을 모두 useContext
를 이용해 가지고 올 수 있도록 하였다.
./Component/TodoList.js
하위 컴포넌트들은props
로 건내받던tasks , dispatch
등을context
를 이용해 가져올 수 있도록 수정한다.
useReducer
와 createContext
를 함께 이용하기App
컴포넌트를 보면 상위 환경의 컴포넌트인 tasks , dispatch
를 useReducer
를 이용해 만들어주는데
만든 tasks , dispatch
는 모두 taskContext , dispatchContext
에게 props
로 전달해주기 위함뿐이다.
그렇다면 이는 TaskContext
에 정의된 Context
들에게 필요한 내용이기 때문에
이는 오히려 TaskContext
에서 선언해주는 것이 컴포넌트의 역할을 명확하게 해줄 수 있을 것이다.
./Core/TaskContext.js
TaskProvider
이라는 컴포넌트를 생성하여 export
하도록 한다.
TaskProvider
컴포넌트는 Context.Provider
들로 {children}
을 감싸 {children}
으로 들어오는
하위 컴포넌트들에게 Context
들을 전달 할 수 있게 만들어주는 Wrapper Component
이다.
이 때 해당 파일에서 생성한 useContext , useDispatch
커스텀 훅 또한 export
하여 하위 컴포넌트에서
TaskProvider
컴포넌트에서 제공하는 taskContext, dispatchContext
의 값을 받아 올 수 있도록 한다.
./Component/TodoList.js
커스텀훅을import
하여 다음처럼TaskProvider
컴포넌트가 제공하는Context
들을 받아오게 수정해줄 수 있다.이는
import {taskContext , dispatchContext} from ../Core/TaskContext
받아온 후useState
를 이용하는 것과 동일하다.이와 같이
use..
로 해당하여 만든 함수를 Custom Hook 이라고 한다.나는 아직 이 부분에 대해 공부하지 않았지만 내용이 궁금하다면 공식 문서를 읽어보기를 권장한다.
App.js
그럼 최종적으로 완성된 컴포넌트는 다음과 같다.
이전에 비해 App
컴포넌트는 렌더링만 하는 한 가지 기능을 할 수 있도록 설계된 것이 보인다.
이전과 비교해보면 얼마나 컴포넌트가 퓨어해졌는지 이해 할 수 있다.
리팩토링 이전의
App
컴포넌트
와 진짜 배울게 많구나
하지만 배우면 배울 수록 좀 더 나도 리액트스럽게 ? 생각 하도록 머리가 개조되는 기분이라 좋다.
오늘 공부한 것은 워낙 어려웠던지라 자기 전에 곰곰히 계속 곱씹어 보며 생각해야겠다.
잘 보고 있습니다..! 도움 많이되네요 ㅎㅎ