React Learn - UI 스케일 확장을 위해 useContext 와 useReducer 를 이용하기

ChoiYongHyeun·2024년 2월 25일
2

리액트

목록 보기
9/31
post-thumbnail

해당 글은 리액트 공식문서인 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 , dispatchProps Drilling 을 효과적으로 해결하기 위해 useContext , useReducer 를 이용하여 리팩토링 해보자


파일 디렉토리 구조 변경하기

처음 토이프로젝트를 만들 때에는 파일 디렉토리 구조를 짜기 귀찮아서 그냥 한 페이지에 우다다 넣어버렸는데

사실은 이렇게 하면 안된다.

이는 컴포넌트, 모듈 기반 UI 구성에 맞지 않는 방법이다.

컴포넌트 모듈화의 필요성

1. 가독성과 유지보수성 향상

컴포넌트들을 pure 하게 관리하기 위해서 각자 독립적인 파일에 관리함으로서

코드의 가독성과 유지 보수성을 높이기 위해 파일 하나에 한 컴포넌트나 관련된 컴포넌트 몇 가지만 같이 넣어두는 것이 모듈화이다.

위 예시처럼 한 페이지에 모든 컴포넌트를 구성하여 한 컴포넌트를 export 하는 행위는 모듈화에 맞지 않는다.

2. 명시적인 의존성 관리

컴포넌트가 다른 컴포넌트가 필요 없이 스스로 존재 할 수 있는 최하위 컴포넌트도 있지만

여러 컴포넌트의 조합으로 만들어지는 컴포넌트도 존재한다.

이러한 컴포넌트는 조합에 사용되는 재료 컴포넌트들과 의존성을 가지고 있다.

재료 컴포넌트라는 말은 실제로 존재하는 말이 아니다. 그냥 내가 이해하기 위해 쓴 개념 키킥

하지만 파일 별로 모듈화 하여 관리 할 때 의존하고 있는 재료 컴포넌트들을 import 하는 행위를 통해

해당 컴포넌트와 의존성있는 컴포넌트를 명시적으로 관리 할 수 있다.

3. 팀 작업의 용이성

팀 작업 시 각자가 담당한 컴포넌트를 파일 단위로 분리하면 각자의 역할에 따라 코드를 분리하여 작업 할 수 있다.

이는 현업을 원활하게 하고 코드 충돌을 최소화 하는데 도움이 된다.

파일 디렉토리 나누기

파일 디렉토리를 다음처럼 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 컴포넌트는 하위 컴포넌트의 로직들을 정의하지 않고, 단순히 현재의 statetasksdispatch 만을 생성하여 props 로 내려주고 있기 때문에

역할을 명확히 알아보기 쉽다.

./Component/ToDoHeader.js

컴포넌트의 조합으로 만들기 위해 App 컴포넌트가 반환하던 내용을 컴포넌트화하여 만들어주었다.

해당 부분의 모습은 다음과 같이 생겼다.

이후는 수정이 일어난 부분들에 대해서만 첨부하도록 하겠다.

./Component/Input.js

./Component/TodoInput.js

./Component/TodoText.js

./Component/TodoList.js

모듈화를 모두 하고 컴포넌트를 퓨어하게 만드니 각 컴포넌트의 역할을 명확하게 알기 쉬워졌다.


useContext 를 이용하기

컴포넌트를 퓨어하게 만들었다고 하더라도 여전히 App 컴포넌트의 반환값은

렌더링과 하위 컴포넌들에게 props 를 건내주는 모습은 동일하다.

이뿐만 아니라 TodoList 컴포넌트의 경우에는

여전히 TodoInputTodoText 부분에 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 를 이용해 가져올 수 있도록 수정한다.


useReducercreateContext 를 함께 이용하기

App 컴포넌트를 보면 상위 환경의 컴포넌트인 tasks , dispatchuseReducer 를 이용해 만들어주는데

만든 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 컴포넌트


회고

와 진짜 배울게 많구나

하지만 배우면 배울 수록 좀 더 나도 리액트스럽게 ? 생각 하도록 머리가 개조되는 기분이라 좋다.

오늘 공부한 것은 워낙 어려웠던지라 자기 전에 곰곰히 계속 곱씹어 보며 생각해야겠다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

2개의 댓글

comment-user-thumbnail
2024년 5월 13일

잘 보고 있습니다..! 도움 많이되네요 ㅎㅎ

1개의 답글