이번 공부 내용은 리액트 공식문서인 Passing Data Deeply with Context 를 중점으로 하여 공부해보았다.
컴포넌트를 구성하다보면
상위 컴포넌트에서 깊은 하위에 존재하는 컴포넌트에게 props
를 전달해주기 위해
하위 컴포넌트를 감싸고 있는 중단 컴포넌트에게 props
를 건내줘야 하는 경우가 발생하곤 한다.
이전에 만들었던 투 두 리스트 만들기 토이프로젝트에서의 예시가 딱 그렇다.
ToDoList
컴포넌트는 tasks
라는 props
를 받도록 하는데
ToDoList
컴포넌트는 tasks
라는 props
를 받아 사용하기 보다
하위 컴포넌트인 ToDoInput
이나 ToDoText
컴포넌트에게 props
를 전달만 해주기 위해
tasks props
를 인수로 받아야 한다.
위 예시는 컴포넌트의 깊이가 얕기 때문에 크게 문제 삼아지지는 않을 수 있지만
전달 받고자 하는 컴포넌트가 하단에 위치 할 수록 문제는 더욱 커진다.
Props Drilling
LeafComponent
에게 PassingProps
를 건내주기 위해서 First , Second , Thrid Component
에서
PassingProps
를 받고 있는 모습을 볼 수 있다.
본인에게 필요하지도 않은 props
를 하위 컴포넌트에 전달하기 위해서 말이다.
이러한 경우를 Props Drilling
이라고 한다 .
Props Drilling
의 단점Props Drilling
이 있을 경우의 문제는 무엇일까 ?
BoilerPlate
코드 증가보일러 플레이트 코드란 어떤 단순한 작동을 위해 불필요한 코드가 반복적으로 나탄는 것을 의미한다.
위 예시에서는 LeafComponent
에게 props
를 전달하기 위해
위의 모든 컴포넌트에게 PassingProps
를 인수로 받도록 하여 전달하는 반복적인 코드 양상이 나타난다.
이는 코드를 장황하게 만들고 가독성을 떨어뜨릴 수 있다.
컴포넌트의 트리 구조를 변경 할 때 마다 Drilling
되는 props
들을 관리하기 위해 신경써야 한다.
이는 유지보수를 어렵게 하고 코드베이스를 변경 할 때 더 많은 작업을 필요로 한다.
리액트는 re-rendering
해야 할 다양한 조건 중 하나인 Props
가 변경 됐을 때 해당 Props
를 인수로 받는
모든 컴포넌트를 리렌더링 한다.
이는 위 예시에서 LeafComPonent
에게 전달할 props
를 변경만 하더라도, FirstComponent
부터 LeafComponent
까지의 모든 컴포넌트가 리렌더링 된다는 것을 의미하며 이는 성능을 저하시킨다.
컴포넌트간 결합도가 증가한다는 것은 컴포넌트간 서로 의존적이게 되어 강하게 결합된 상태를 의미한다.
LeafComponent
까지 향하는 FirstComponent ...
이하의 컴포넌트들은 서로 PassingProps
를 전달하기 위해
강하게 결합되어 있다.
이는 연결되어 있는 컴포넌트간 의존성으로 인해 특정 컴포넌트만을 추출하여 다른 컴포넌트에서 사용하거나
특정 컴포넌트 하나만 수정하는 것이 필연적으로 힘들어진다는 의미이다.
PropsDrilling
을 해결하기 위해 리액트에서 제공하는 Hook
이런 Props Drilling
과 같은 문제를 해결하기 위해 리액트에서는 Context
라는 개념을 가진 Hook
을 제공한다.
먼저 예시를 통해 알아보자
CreateContext
react.CreateContext
를 이용해 Context
를 생성해준다.
생성된 Context
객체는 컴포넌트들이 전역적으로 데이터를 공유 할 수 있게 해주는 메커니즘이다.
LeafComponent
에서 전달받고자 하는 props
를 전역적으로 공유 할 수 있게 해주었으니 모든 컴포넌트에서
passingProps
부분을 모두 제거해주자
이후 rootComponent
에 해당하는 FirstComponent
를 이전에 생성한 LeafContext.Provider
컴포넌트로 감싸주자
이렇게 감싸주면 FirstComponent
하위에 존재하는 모든 컴포넌트들은 LeafContext
에서 정의된 value
값을 사용 할 수 있게 된다.
createContext(defaultValue)
createContext
에서 쓰이는 맨 처음 defaultValue
값은 Context.Provider
를 이용 할 때
value
값이 지정되지 않았을 때 사용되는 디폴트 값이다.
만약 사용시 value
값을 지정해준다면 defaultValue
값이 아닌 지정한 value
값이 사용된다.
useContext
useContext
는 해당 컴포넌트의 상위 컴포넌트에서 정의된 Context.Provider
의 value
값을 가져온다.
위에서는 App
컴포넌트에서 정의된 LeafContext.Provider
가 제공하는 value
값을 가져와 사용 할 수 있게 된다.
useContext
는 항상 가장 가까운 상위 컴포넌트의 컨텍스트를 이용한다.위 예시처럼 LeafComponent
에게 전달하기 위한 Context
가
Seconde , Thrid , Leaf Component
모두에서 정의되었을 때
LeafComponent
는 자신의 가장 가까운 상위 컴포넌트에서 정의된 context
를 이용한다.
생각하기 앞서 Context
가 왜 필요한지에 대한 회고가 필요하다.
Context
를 이용하면 우선 Props Drilling
을 방지 하여 불필요한 re-rendering
을 막고 컴포넌트 간 의존성을 내린다는 장점이 있다.
컴포넌트간 의존성이 낮을 수록 컴포넌트를 재사용 하기도, 수정하기도 용이하기 때문에
가장 가까운 상위 컴포넌트의 컨텍스트만 이용하도록 하여
최대한 컴포넌트 간 의존성의 깊이를 낮추도록 한다.
만일 다음처럼 FirstComponent
와 ThridComponent
모두 LeafContext.Provider
를 통해 Context
를 제공하고
본인이 가지고 있는 state
값에 따라 전달하고자 하는 Context
의 값이 변경된다고 해보자
앞서 설명했듯이 LeafComponent
는 가장 가까운 상위 컴포넌트의 Context
를 이용하기 때문에
FirstComponent
의 Context
변화에는 반응하지 않는 모습을 볼 수 있다.
이를 통해 LeafComponent
는 FirstComponent
가 아닌 가장 가까운 부모 컴포넌트인 ThridComponent
상태에만
의존하기 때문에
결합도의 깊이가 낮아진 모습을 볼 수 있다.
Context
를 너무 남용하지는 마세요위 게시글만 보면
ㅋㅋ 아 ~
state
를props
로 뭐하로 넘겨주냐고 ~context
이용하면 되는데 ~ ㅋㅋ
라는 생각이 든다.
하지만 context
는 Props Drilling
이 발생 할 때 사용할 수 있는 대안책 역할이지
Props Drilling
을 사용해도 좋다는 것은 아니다.
Context
를 남용 할 때 생기는 단점Props
를 건내주는 흐름을 추적하기가 어렵다.Context
를 이용하면 value
값으로 넘겨줄 Props
값이 전역에서 접근 가능하기 때문에
state
의 변화에 따른 컴포넌트의 변화를 추적하기 어렵다는 단점이 있다.
기존 Props
를 건내주는 방식은 각 계층별로 하위 컴포넌트에게 폭포처럼 하위로 전달해주기 때문에
컴포넌트의 계층적 구조를 통해 흐름을 이해 하기 편하지만
Context
의 경우는 계층적 구조를 무시하고 전달하기 때문에 건내주는 흐름을 추적하기가 어렵다.
Context
를 구독하고 있는 컴포넌트가 많을 수록 가독성이 떨어진다.만일 하위 컴포넌트에게 props
를 전달해줄 때 직접 전달하는 것이 아니라
모두 Context
를 이용해서 전달하려고 모두 Context
를 이용해서 전달한다면
불필요한 코드량이 늘어날 뿐 아니라 상위 Context
를 확인하기 위해 여기 저기 컴포넌트들을 확인해야 하는 일이 발생한다.
Context
를 사용하기 전 시도해 볼 수 있는 방법들Context
를 이용하는 이유는 Props Drilling
을 최대한 대안적으로 해결하기 위함이였는데
가장 좋은 방법은 Props Drilling
을 지양하는 것이 가장 중요하다.
Props Drilling
이 발생 할 것 같다면 해당 데이터를 상위 컴포넌트에서 정의하지 말자하위 컴포넌트에게 건내주고자 하는 props
가 매우 높은 위치의 부모 컴포넌트로부터 전달 받아야 한다면
데이터를 하위 컴포넌트에서 생성하고 관리하도록 이용하든지
해당 데이터를 이용한 로직을 최대한 지양해야 한다.
상위 컴포넌트로부터 Drilling
되어 내려오는 데이터는 모든 컴포넌트의 의존성을 높혀
컴포넌트의 관리성을 매우 낮춘다.
Props Drilling
이 필연적으로 발생 해야 한다면 children props
를 잘 이용해보자이전 Props Drilling
에서 passingProps
를 계속하여 Drilling
하여 보내게 됐던 가장 큰 이유는
최상단 컴포넌트에서 FirstComponent
를 하나만 호출하도록 설계되었기 때문이다.
한 눈에 어떤 컴포넌트에게 props
를 건내주고 있는지를 한 눈에 알 수 있다면
Props Drilling
으로 인해 발생하는 문제를 해결 할 수 있을 것이다.
각 컴포넌트들이 children
을 가질 수 있도록 하게 하여 최상단 컴포넌트인 App
컴포넌트에서
props
를 건내준다면 훨씬 컴포넌트의 계층적 구조를 이해하고
컴포넌트간 어떤 props
를 받고 있는지 한 눈에 이해하기 쉬워질 것이다.
Context
의 Use Case
위 예시들을 보면 또
ㅋㅋ 아 ~ Context
사용 안하는게 훨씬 낫네 ~ Context
왜 씀 ?
이런 편협한 생각이 들곤 한다.
내가 그랬다.
그럼에도 불구하고 useContext
가 유용하게 사용되는 몇 가지가 존재한다.
특히 모든 컴포넌트가 일괄적으로 동일한 state
를 받아야 한다면, 모두에게 props
로 건내주기 보다 Context
로 정의해두고 컴포넌트 내에서 해당 Context
를 호출하는 것이 훨씬 효율적이다.
Theming
테마의 일부인 다크모드의 경우 UX
를 높혀주는 방법 중 하나로 사용자가 특정 버튼을 누르면
모든 컴포넌트들이 해당 테마에 맞춰 리렌더링 된다.
페이지의 테마를 전역에서 Context
로 정의해두고 각 컴포넌트 내에서 호출하여 사용한다면 훨씬 효과적일 것이다.
Current Account
페이지에서 클라이언트가 로그인을 하고 있는지, 하고 있지 않은지에 따라 렌더링 하는 영역이 다를 것이다.
내용도 다를 것이고 말이다.
이에 페이지의 현재 상태를 나타내는 것 또한 전역적인 Context
로 정의해두고
컴포넌트 내부에서 받아 호출한다면 효과적일 것이다.
Routing
아직 나는 리액트 라우터에 대한 내용을 배우지 않았다.
이 부분은 리액트 공식문서의 내용을 추가하도록 하겠다.
Routing: Most routing solutions use context internally to hold the current route.
This is how every link “knows” whether it’s active or not.
If you build your own router, you might want to do it too.
Managing State
어플리케이션이 커지면 커질 수록 , 상단에 있는 state
를 하위 컴포넌트에서 수정하는 경우가 잦아질 것이다.
이런 경우에도 useContext
를 이용하면 효과적으로 상태를 변경할 수 있다.
이는 다음 챕터에서 자세히 공부해보도록 하자
Recap
Context
는 컴포넌트들이 전역적으로 관리되는 데이터들을 받을 수 있도록 한다. (상위 컴포넌트에서 제공하는)Context
를 passing
하기 위해서는createContext
를 이용해 passing
할 Context
를 생성한다.Context
를 이용할 컴포넌트 내에서 useContext
로 상단에서 정의된 Context
를 받아온다.Context
를 제공할 컴포넌트는 children
을 <Context.Provider value = {}>
등으로 감싸준다.Context
는 컴포넌트의 위치에 상관 없이 어디서나 Provider
형식으로 Context
를 제공 할 수 있다.Context
를 이용하면 컴포넌트의 최상위 컴포넌트에게만 props
를 전달받는 딱딱한 계층적 구조에서 탈피하여 주위에 있는 컴포넌트로부터 props
를 받을 수 있도록 하게 도와준다.Context
를 이용하기 전 props
를 전달하는 방식이 없는지 먼저 생각해보자 . props
를 이용하도록 하면 컴포넌트의 계층적 구조를 한 눈에 이해하기 쉬워질 것이다.