From 68568b7cda8dfa62f7ac72e956c5c497f8e72b74 Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 26 Mar 2026 17:18:02 +0900 Subject: [PATCH 01/14] =?UTF-8?q?docs:=20provider=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/provider.md | 116 +++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 log/data/kong/method/react/provider.md diff --git a/log/data/kong/method/react/provider.md b/log/data/kong/method/react/provider.md new file mode 100644 index 0000000..e02c98c --- /dev/null +++ b/log/data/kong/method/react/provider.md @@ -0,0 +1,116 @@ +# 프로-바이더 + +## 서론 + +하,,,, + +분명? 나는 리액트 컴포넌트의 성능을 런타임에도 적은 오버헤드로 telemetry 할 수 있는 프로파일링 라이브러리를 만들고 있었는데, 해당 라이브러리를 그냥 쌩으로 만들기에는 지식이 너무 없었다. + +그래서 claude에게 설계를 보여주며 짜달라고 했고, 해당 코드들을 분석 후 다시 처음부터 내가 만들려고 했다. + +그런데 코드 분석 중 `createContext`과 `useContext`을 맞닥뜨리게 되었다… 그게 뭔데? `useState`나 `useRef`같은 것들만 사용해오던 나에게는…. 너무 어려웠다….n + +공식 문서를 자세하게 읽어봐도 잘 모르겠어서 아는 프론트엔드 개발자분들께 여쭤보니 context가 최상단에서 상태를 저장해두고 상단에서 context provider를 설정해두고 쏴주는거라고,,, 약간 전역 변수처럼 쓰이는 친구인가보다... + +> Redux, recoil, tq의 조상격(?),,,? 전역으로 변수 설정이 가능한 친구... +> useContext with Providers 참고.... +> 전역 라이브러리와 context API 차이... +> provider 컴파운드 컴포넌트도 따로 분리... + +예...? provider가 뭔데요...? + +--- + +## 그래서 provider는 또 뭔데? (context.provider) + +```js + +``` + +리액트로 컴포넌트를 만들 때 상태값 관리는 보통 props나 state로 관리한다. +이외에도 상태 관리 라이브러리가 있다. + +context에 포함된 react provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 한다. +provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달한다. 값을 전달받을 수 있는 컴포넌트 수에 제한은 없다. +provider 하위에 또 다른 provider를 배치하는 것도 가능하며, 이 경우 하위 provider의 값이 우선시된다. + +Provider의 역할은 우리의 App이 Redux.store에 접근할 수 있도록 해준다. +그러니까 provider는 HOC로 context를 제공하고, react가 제공하는 createContext 메서드를 활용하여 context 객체를 만들어낼 수 있다. +Provider 컴포넌트는 value라는 prop으로 하위 컴포넌트들에게 내려줄 데이터를 받는다. 이 컴포넌트의 모든 자식 컴포넌트들은 해당 provider를 통해 value prop에 접근할 수 있다. + +요약: provider 안에 있는 자식 컴포넌트들은 provider 라인에서 선언한 context를 value와 useContext를 통해 받을 수 있다. + +```js +function App() { + const data = { ... } + + return ( +
+ + + + +
+ ) +} + +function SideBar() { + const { data } = React.useContext(DataContext); + return

{data.text}

; +} + +``` + +이런 식으로 각 컴포넌트에서 useContext를 import 하는 대신 필요로 하는 컨텍스트를 직접 반환하는 훅을 구현할 수 있다. + +```js +function useThemeContext() { + const theme = useContext(ThemeContext); + if (!theme) { + throw new Error("useThemeContext must be used within ThemeProvider"); + } + return theme; +} +``` + +--- + +가만히 보면 그냥 context랑 별반 다를 게 없는 친구 아니야? 싶겠지만, 그건 내가 이해를 잘못 해서 그런 것이었다. +provider와 context는 역할이 다르다... +provider는 값을 넣는 쪽, context는 값을 꺼내는 쪽이라고 보면 될 것 같다. +Provider 없이 useContext만 쓰면 값이 null이고, useContext 없이 Provider만 쓰면 값을 꺼내지 못한다... + +```js +const UserContext = createContext(null); + +// Provider → 값을 "넣는" 쪽 + + +; + +// useContext → 값을 "꺼내는" 쪽 +function GrandChild() { + const user = useContext(UserContext); // 꺼냄 +} +``` + +--- + +기존에 provider를 사용할 때는 ~context.provider를 했어야 했는데, React 19로 올라오면서 `` 대신 ``를 바로 Provider로 렌더링할 수 있다. + +```js +// React 18 이하 (구버전) + + + + +// React 19 이상 (신버전) — 완전히 동일한 동작 + + + +``` + +## 참고 + +[Provider 패턴](https://patterns-dev-kr.github.io/design-patterns/provider-pattern/) +-> 괜찮은 기술?블로그입니다. 참고하시면 좋을 것 같아요. From 4e403d9ccaa1480f3537158bfc0d7bd996b78dfa Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 26 Mar 2026 17:29:35 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EB=AC=B8=EB=A7=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/provider.md | 35 +++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/log/data/kong/method/react/provider.md b/log/data/kong/method/react/provider.md index e02c98c..c4dc601 100644 --- a/log/data/kong/method/react/provider.md +++ b/log/data/kong/method/react/provider.md @@ -33,8 +33,8 @@ context에 포함된 react provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 한다. provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달한다. 값을 전달받을 수 있는 컴포넌트 수에 제한은 없다. provider 하위에 또 다른 provider를 배치하는 것도 가능하며, 이 경우 하위 provider의 값이 우선시된다. +Redux도 내부적으로 같은 Provider 패턴을 사용한다. -Provider의 역할은 우리의 App이 Redux.store에 접근할 수 있도록 해준다. 그러니까 provider는 HOC로 context를 제공하고, react가 제공하는 createContext 메서드를 활용하여 context 객체를 만들어낼 수 있다. Provider 컴포넌트는 value라는 prop으로 하위 컴포넌트들에게 내려줄 데이터를 받는다. 이 컴포넌트의 모든 자식 컴포넌트들은 해당 provider를 통해 value prop에 접근할 수 있다. @@ -61,27 +61,17 @@ function SideBar() { ``` -이런 식으로 각 컴포넌트에서 useContext를 import 하는 대신 필요로 하는 컨텍스트를 직접 반환하는 훅을 구현할 수 있다. - -```js -function useThemeContext() { - const theme = useContext(ThemeContext); - if (!theme) { - throw new Error("useThemeContext must be used within ThemeProvider"); - } - return theme; -} -``` - --- 가만히 보면 그냥 context랑 별반 다를 게 없는 친구 아니야? 싶겠지만, 그건 내가 이해를 잘못 해서 그런 것이었다. -provider와 context는 역할이 다르다... -provider는 값을 넣는 쪽, context는 값을 꺼내는 쪽이라고 보면 될 것 같다. +provider와 context는 역할이 다르다... provider는 값을 넣는 쪽, context는 값을 꺼내는 쪽이라고 보면 될 것 같다. Provider 없이 useContext만 쓰면 값이 null이고, useContext 없이 Provider만 쓰면 값을 꺼내지 못한다... +그리고 Provider가 감싼 범위만 전역변수로 사용할 수 있으니 참고할 것. ```js const UserContext = createContext(null); +// Provider가 없을 때 사용되는 기본값 +// Provider 안에 있으면 이 값은 무시됨 // Provider → 값을 "넣는" 쪽 @@ -110,6 +100,21 @@ function GrandChild() { ``` +## 번외. 훅으로 만들기! + +이런 식으로 각 컴포넌트에서 useContext를 import 하는 대신 필요로 하는 컨텍스트를 직접 반환하는 훅을 구현할 수 있다. +커스텀 훅으로 감싸면 에러 메시지도 넣을 수 있고, 매번 import를 두 번 안 해도 된다...! + +```js +function useThemeContext() { + const theme = useContext(ThemeContext); + if (!theme) { + throw new Error("useThemeContext must be used within ThemeProvider"); + } + return theme; +} +``` + ## 참고 [Provider 패턴](https://patterns-dev-kr.github.io/design-patterns/provider-pattern/) From 193a90542a21d678fd3383e9e04cca4329d8bf23 Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 26 Mar 2026 17:35:46 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20provider=20=EB=B2=88=EC=99=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/provider.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/log/data/kong/method/react/provider.md b/log/data/kong/method/react/provider.md index c4dc601..94967a9 100644 --- a/log/data/kong/method/react/provider.md +++ b/log/data/kong/method/react/provider.md @@ -28,7 +28,7 @@ ``` 리액트로 컴포넌트를 만들 때 상태값 관리는 보통 props나 state로 관리한다. -이외에도 상태 관리 라이브러리가 있다. +여기서 context가 나오는데, context는 간단하게 props drilling 문제를 해결하기 위한 React의 내장 기능이라고 보면 될 것 같다. context에 포함된 react provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 한다. provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달한다. 값을 전달받을 수 있는 컴포넌트 수에 제한은 없다. @@ -115,6 +115,10 @@ function useThemeContext() { } ``` +## 번외2. 검색 + +검색할 때 react provider 라고 하면 나오는 게 많이 없다... provider 패턴이라고 검색해야 잘 나온다. 참고하기! + ## 참고 [Provider 패턴](https://patterns-dev-kr.github.io/design-patterns/provider-pattern/) From bcc63f8291b8430349674e07ea39b0d89a261331 Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 2 Apr 2026 16:16:30 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20context=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/context.md | 208 ++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 log/data/kong/method/react/context.md diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md new file mode 100644 index 0000000..2a9c126 --- /dev/null +++ b/log/data/kong/method/react/context.md @@ -0,0 +1,208 @@ +# 콘-텍스트 + +> [!Info] +> 연관된 파일로는 `provider.md`가 있으니 시간이 난다면 한 번 보는 것도 좋을 것 같다. + +어떤 라이브러리를 만들기 위해 AI에게 코드 작성과 에러 수정 후 코드 분석을 하게 된 나... 시작부터 난관에 맞닥뜨리는데... + +(대충 놀랍고 엄청나다는 이미지) + +--- + +## 서론 + +코드를 분석하기 위해 `import` 부분부터 봤다. 뭐든 처음부터 보는 게 좋지 않은가... 그래서 코드를 봤는데 바로 모르는 것이 나와버렸다... `createContext`, `useContext`. 이게 대체 뭐지? + +--- + +## Props + +들어가기 전에 Context를 이해하려면 먼저 props를 알면 도움이 된다. + +간단하게 설명하자면, 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달하는 방식이 props다. props는 간단하게 데이터를 전달할 수 있지만, 자식 컴포넌트 이상으로 데이터를 전달하려면 중간 단계의 컴포넌트에서 불필요하게 데이터를 처리해야 하는 문제가 발생한다. + +![alt text](https://somwpkzlplaovldnfahk.supabase.co/storage/v1/object/public/heropy.dev_posts/EdhHX2/s2.JPG) + +이런 상황을 속성이 여러 컴포넌트를 관통하는 것 같다고 해서 **Prop Drilling**이라고 한다. + +--- + +## 그래서 진짜로, Context. 그게 뭔데 + +해당 props drilling을 해결하기 위해 나온 게 바로 Context API다. + +Context API는 앱에서 컴포넌트로 **props를 사용하지 않고 필요한 데이터를 쉽게 공유**할 수 있게 해준다. 특정 컴포넌트에서 제공하는 데이터를 하위 컴포넌트에서 사용할 수 있게 하는 것 — 중간 컴포넌트들을 거치지 않고, 필요한 컴포넌트가 직접 값을 꺼내 사용한다. + +```tsx +import { createContext, useContext } from "react"; + +// 1. createContext 메서드로 context 생성 +const MyContext = createContext(데이터의초기값); + +// 2. Provider로 대상 컴포넌트를 감싸고, value에 전달할 데이터를 넣는다 +{children}; + +// 3. 필요한 컴포넌트에서 useContext로 꺼내 쓴다 (공식 문서 권장 방식) +const 데이터 = useContext(MyContext); + +// 참고: Consumer 컴포넌트 방식도 있지만, useContext 사용을 권장한다 +{(데이터) =>
{데이터}
}
; +``` + +실제 예제로 보면: + +```tsx +import { createContext, useContext } from "react"; + +const ThemeContext = createContext(null); + +export default function MyApp() { + return ( + +
+ + ); +} + +function Panel({ title, children }) { + const theme = useContext(ThemeContext); // "dark"를 받아옴 + return ( +
+

{title}

+ {children} +
+ ); +} + +function Button({ children }) { + const theme = useContext(ThemeContext); // 여기서도 "dark"를 받아옴 + return ; +} +``` + +--- + +## 주로 어디에 사용할까? + +Context가 적합한 데이터 종류가 있다. + +- **라이트 모드 / 다크 모드** 설정 +- **사용자 데이터** (현재 인증된 유저 정보) +- **언어 혹은 지역 데이터** + +공통점이 보이는가? 모두 **자주 업데이트할 필요가 없는 데이터**다. Context에서 `value`가 바뀌면 Provider로 감싼 모든 자식 컴포넌트들이 리렌더링되므로, 자주 바뀌는 값에는 적합하지 않다. + +즉 Context는 *컴포넌트를 위한 전역 변수*의 개념이라고 볼 수 있다. + +--- + +## Context를 사용하기 전에 고려할 것 + +Context는 사용하기에 꽤 유혹적이다. 그러나 이는 또한 남용하기 쉽다는 의미이기도 하다. **어떤 props를 여러 레벨 깊이로 전달해야 한다고 해서 해당 정보를 context에 넣어야 하는 것은 아니다.** + +먼저 이 두 가지를 시도해보자. + +**1. Props 전달하기로 시작하기.** 여러 컴포넌트를 거쳐 props가 흘러가는 것은 그리 이상한 일이 아니다. 힘든 일처럼 느껴질 수 있지만, 어떤 컴포넌트가 어떤 데이터를 사용하는지 매우 명확히 해줘서 유지보수하기 좋다. + +**2. 컴포넌트를 추출하고 `children`으로 전달하기.** 이게 바로 Composition(합성)이라는 기법이다. 데이터를 쓰지도 않는 컴포넌트가 짐꾼 역할을 하고 있다면, 구조를 바꾸는 것이 먼저다. + +```tsx +// Bad: Layout이 user를 쓰지도 않는데 받아야 함 +function App() { + const [user] = useState({ name: "홍길동" }); + return ; +} +function Layout({ user }) { + return ( +
+
+
+ ); // 그냥 전달만... +} + +// Good: App에서 Header를 직접 렌더링해서 넣어버린다 +function App() { + const [user] = useState({ name: "홍길동" }); + return ( + +
{/* Layout은 user가 뭔지 몰라도 됨 */} + + ); +} +function Layout({ children }) { + return
{children}
; // 그냥 구멍(Slot)만 뚫어놓으면 끝 +} +``` + +`App` → `Layout` → `Header` 3단계였던 게, `App` → `Header`로 줄어든다. Layout은 껍데기가 되어 리렌더링 범위에서도 제외된다. + +이래도 해결이 안 될 만큼 깊거나 광범위할 때 비로소 Context를 꺼내 드는 것이 React의 권장 순서다. + +--- + +## Context랑 전역 상태 관리 라이브러리, 같은 거 아냐? + +겉보기에는 Context API도 전역적으로 데이터를 뿌려주니까 Redux나 Zustand 같은 라이브러리의 대체제처럼 보일 수 있다. 허나 다르다. + +**Context API의 핵심 설계 목적은 Dependency Injection(의존성 주입)이다.** 깊게 중첩된 컴포넌트 트리에서 Prop Drilling 없이 데이터를 하단으로 꽂아주는 **통로** 역할. + +반면 상태 관리 도구는 단순히 값을 전달하는 것 외에 **효율적인 업데이트**가 가능해야 한다. Context의 치명적인 약점이 여기서 드러난다. + +``` +전역 상태 객체 { user, theme, posts }가 Context에 있다고 가정. +→ theme만 바뀌어도 user 정보만 쓰는 컴포넌트까지 전부 재렌더링된다. +``` + +Redux, Zustand, Recoil 같은 라이브러리는 내부적으로 최적화되어 있어서, `user`가 바뀌어도 `theme`을 구독 중인 컴포넌트는 눈 하나 깜짝 안 한다. 이게 바로 **전달**과 **관리**의 차이다. + +| 특징 | Context API | 전역 상태 관리 라이브러리 | +| ------------- | ------------------------------ | ------------------------------------ | +| 주 목적 | Props 전달 생략, 결합도 낮추기 | 효율적인 상태 업데이트 및 로직 분리 | +| 렌더링 최적화 | 어려움 (구독자 전체 렌더링) | 뛰어남 (변경된 부분만 선택적 렌더링) | +| 비동기 처리 | 직접 구현해야 함 | Middleware 등 내장/지원 | +| 디버깅 툴 | 기본 DevTools | 전용 DevTools (타임머신 디버깅 등) | + +요약하면: + +- **Context** → 데이터를 어디로 보낼지 결정하는 **전송 수단** +- **Redux·Zustand** → 데이터를 어떻게 관리하고 효율적으로 변화시킬지 결정하는 **시스템** + +--- + +## 그래서 언제 뭘 써야 할까? + +**Context가 적합한 경우:** 값이 자주 바뀌지 않을 때 (테마, 언어, 로그인 유저 정보), 앱 전체보다는 특정 범위 내에서만 공유가 필요할 때. + +**상태 관리 라이브러리가 적합한 경우:** 업데이트가 빈번하게 일어나는 복잡한 데이터, 컴포넌트 수백 개 중 특정 컴포넌트만 정밀하게 업데이트해야 할 때, 상태 관리 로직을 컴포넌트 외부로 완전히 분리하고 싶을 때. + +--- + +## 고민 + +1. context가 props drilling 해결을 위해 사용하는 건 맞는데, 렌더링 측면에서 보면 context가 리렌더링을 많이 일으켜서 그렇게 좋다고는 생각을 안 함… 근데 어차피 자식의 자식의 자식으로 보내서 뭘 해도 리렌더링 일으키는 거, 그냥 편하게 최상단에서 context로 쏴버리자인가? +2. 근데 생각해보니 리렌더링은 값이 변화할 때만 일어나는 것이고… context가 변화하는 값에만 사용되는 게 아니라 그냥 변화하지 않는 일반적인 값을 내려줄 때도 사용되니… 양측에서 생각을… +3. 근데 거기서 더 생각해보니 안에 들어 있는 모든 컴포넌트가 리렌더링 되는 게 아니라 `useContext`를 사용하고 있는 애들만 리렌더링 되니까… + +--- + +## 별록 + +`useContext`는 전달한 Context에 대한 Context Value를 반환한다. Context 값을 결정하기 위해 React는 컴포넌트 트리를 탐색하고, 특정 Context에 대해 상위에서 가장 가까운 Context Provider를 찾는다. + +`value`에 `useState` 값을 넣으면 Context를 업데이트할 수 있다. + +```tsx +const AppContext = createContext(); + +export const AppProvider = ({ children }) => { + const [user, setUser] = useState({ name: "Guest" }); + + return {children}; +}; +``` + ++) 변하는 값에는 전역 상태 관리 라이브러리, 그저 props에 값을 전달할 때는 context도 괜찮을 것 같다. + +--- + +\_참고: [heropy.dev](https://heropy.dev), [React 공식 문서](https://ko.react.dev/learn/passing-data-deeply-with-context) From 3c0a03dca5d46737330856edee05e89680d9a0ea Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 2 Apr 2026 16:23:53 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20context=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/context.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md index 2a9c126..05b39a8 100644 --- a/log/data/kong/method/react/context.md +++ b/log/data/kong/method/react/context.md @@ -25,6 +25,8 @@ 이런 상황을 속성이 여러 컴포넌트를 관통하는 것 같다고 해서 **Prop Drilling**이라고 한다. +image + --- ## 그래서 진짜로, Context. 그게 뭔데 From 2b13a5abc7ba9c0963ac27960f40c140c2628314 Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 2 Apr 2026 16:27:14 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/context.md | 2 +- .../react/image/prop_drilling_vs_context.svg | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 log/data/kong/method/react/image/prop_drilling_vs_context.svg diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md index 05b39a8..8b5d7b1 100644 --- a/log/data/kong/method/react/context.md +++ b/log/data/kong/method/react/context.md @@ -25,7 +25,7 @@ 이런 상황을 속성이 여러 컴포넌트를 관통하는 것 같다고 해서 **Prop Drilling**이라고 한다. -image +![Prop drilling vs Context](.//image/prop_drilling_vs_context.svg) --- diff --git a/log/data/kong/method/react/image/prop_drilling_vs_context.svg b/log/data/kong/method/react/image/prop_drilling_vs_context.svg new file mode 100644 index 0000000..a86f83a --- /dev/null +++ b/log/data/kong/method/react/image/prop_drilling_vs_context.svg @@ -0,0 +1,88 @@ + + + + + + + + + Prop drilling + + + + App + + + + color + + + + A1 + + 안 씀 + + + color + + + + A2 + + + + color + + + + A3 + + 씀! + + + + A1·A2가 억지로 넘겨줘야 함 + + + + + + Context 사용 + + + + ThemeContext.Provider + value={{ color }} + + + + + + + + + A1 + + 안 씀 + + + + A2 + + 안 씀 + + + + A3 + + + + + + useContext + + + + 중간 단계 불필요! + + \ No newline at end of file From f6de4e7361c438db5904be7251926a0c394fd106 Mon Sep 17 00:00:00 2001 From: Hyunji0012 <113183900+za0012@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:29:35 +0900 Subject: [PATCH 07/14] Fix image path for Prop Drilling vs Context --- log/data/kong/method/react/context.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md index 8b5d7b1..359300c 100644 --- a/log/data/kong/method/react/context.md +++ b/log/data/kong/method/react/context.md @@ -22,10 +22,12 @@ 간단하게 설명하자면, 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달하는 방식이 props다. props는 간단하게 데이터를 전달할 수 있지만, 자식 컴포넌트 이상으로 데이터를 전달하려면 중간 단계의 컴포넌트에서 불필요하게 데이터를 처리해야 하는 문제가 발생한다. ![alt text](https://somwpkzlplaovldnfahk.supabase.co/storage/v1/object/public/heropy.dev_posts/EdhHX2/s2.JPG) +image + 이런 상황을 속성이 여러 컴포넌트를 관통하는 것 같다고 해서 **Prop Drilling**이라고 한다. -![Prop drilling vs Context](.//image/prop_drilling_vs_context.svg) +![Prop drilling vs Context](./image/prop_drilling_vs_context.svg) --- From 9f7eb8394b6f65ec5f1b4c79ce686f8ab6383031 Mon Sep 17 00:00:00 2001 From: Hyunji0012 <113183900+za0012@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:30:35 +0900 Subject: [PATCH 08/14] Update images related to Prop Drilling explanation Removed an image and added a new image to explain Prop Drilling in React context. --- log/data/kong/method/react/context.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md index 359300c..177d8ba 100644 --- a/log/data/kong/method/react/context.md +++ b/log/data/kong/method/react/context.md @@ -21,13 +21,10 @@ 간단하게 설명하자면, 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달하는 방식이 props다. props는 간단하게 데이터를 전달할 수 있지만, 자식 컴포넌트 이상으로 데이터를 전달하려면 중간 단계의 컴포넌트에서 불필요하게 데이터를 처리해야 하는 문제가 발생한다. -![alt text](https://somwpkzlplaovldnfahk.supabase.co/storage/v1/object/public/heropy.dev_posts/EdhHX2/s2.JPG) -image - +image 이런 상황을 속성이 여러 컴포넌트를 관통하는 것 같다고 해서 **Prop Drilling**이라고 한다. - -![Prop drilling vs Context](./image/prop_drilling_vs_context.svg) +image --- From 1d3f9aefab8ed6ffea01e8ee5929658d4da828df Mon Sep 17 00:00:00 2001 From: Hyunji0012 <113183900+za0012@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:31:06 +0900 Subject: [PATCH 09/14] Add image for Prop Drilling explanation Add an image to illustrate the concept of Prop Drilling. --- log/data/kong/method/react/context.md | 1 + 1 file changed, 1 insertion(+) diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md index 177d8ba..642fa22 100644 --- a/log/data/kong/method/react/context.md +++ b/log/data/kong/method/react/context.md @@ -24,6 +24,7 @@ image 이런 상황을 속성이 여러 컴포넌트를 관통하는 것 같다고 해서 **Prop Drilling**이라고 한다. + image --- From 957d4540bd624f6859d1db442b1d1502b8187ded Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 2 Apr 2026 16:31:47 +0900 Subject: [PATCH 10/14] =?UTF-8?q?chore:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react/image/prop_drilling_vs_context.svg | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 log/data/kong/method/react/image/prop_drilling_vs_context.svg diff --git a/log/data/kong/method/react/image/prop_drilling_vs_context.svg b/log/data/kong/method/react/image/prop_drilling_vs_context.svg deleted file mode 100644 index a86f83a..0000000 --- a/log/data/kong/method/react/image/prop_drilling_vs_context.svg +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - Prop drilling - - - - App - - - - color - - - - A1 - - 안 씀 - - - color - - - - A2 - - - - color - - - - A3 - - 씀! - - - - A1·A2가 억지로 넘겨줘야 함 - - - - - - Context 사용 - - - - ThemeContext.Provider - value={{ color }} - - - - - - - - - A1 - - 안 씀 - - - - A2 - - 안 씀 - - - - A3 - - - - - - useContext - - - - 중간 단계 불필요! - - \ No newline at end of file From 1bff9ac02ca36add3c599615d0959675b8dc74a4 Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 2 Apr 2026 16:35:13 +0900 Subject: [PATCH 11/14] =?UTF-8?q?chore:=20=EB=B3=B4=EA=B8=B0=20=ED=8E=B8?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EC=95=BD=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/context.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md index 642fa22..535df55 100644 --- a/log/data/kong/method/react/context.md +++ b/log/data/kong/method/react/context.md @@ -173,9 +173,16 @@ Redux, Zustand, Recoil 같은 라이브러리는 내부적으로 최적화되어 ## 그래서 언제 뭘 써야 할까? -**Context가 적합한 경우:** 값이 자주 바뀌지 않을 때 (테마, 언어, 로그인 유저 정보), 앱 전체보다는 특정 범위 내에서만 공유가 필요할 때. +**Context가 적합한 경우:** -**상태 관리 라이브러리가 적합한 경우:** 업데이트가 빈번하게 일어나는 복잡한 데이터, 컴포넌트 수백 개 중 특정 컴포넌트만 정밀하게 업데이트해야 할 때, 상태 관리 로직을 컴포넌트 외부로 완전히 분리하고 싶을 때. +- 값이 자주 바뀌지 않을 때 (테마, 언어, 로그인 유저 정보) +- 앱 전체보다는 특정 범위 내에서만 공유가 필요할 때. + +**상태 관리 라이브러리가 적합한 경우:** + +- 업데이트가 빈번하게 일어나는 복잡한 데이터 +- 컴포넌트 수백 개 중 특정 컴포넌트만 정밀하게 업데이트해야 할 때 +- 상태 관리 로직을 컴포넌트 외부로 완전히 분리하고 싶을 때. --- From 1762b75a0f07f5a0a04f0da12a6dccde1ee353c5 Mon Sep 17 00:00:00 2001 From: za0012 Date: Thu, 2 Apr 2026 17:35:37 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20context=20=EA=B3=A0=EB=AF=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/context.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/context.md index 535df55..ef2ba61 100644 --- a/log/data/kong/method/react/context.md +++ b/log/data/kong/method/react/context.md @@ -214,4 +214,25 @@ export const AppProvider = ({ children }) => { --- +## 추가 + +해당 글을 작성하면서 context는 언제 사용하고 왜 전역 상태 관리 라이브러리로 대체하지 않는지 궁금점들이 생겼다. +왜냐하면 context는 리렌더링 측면에서만 봤을 때는 그리 좋지 않기 때문이다. 그래서 리렌더링 최적화가 잘 되어있는 라이브러리를 사용하면 되지 않을까? 생각했다. +그런데 그게 아니었다. + +라이브러리와 context를 나누는 기준은 범위로 생각을 해야한다. context는 A라는 컴포넌트 트리 안에서만 공유되어야 하는 값일 때 사용한다. +따라서 Provider로만 감싼 해당 트리 안에서만 유효한 범위를 만들 수 있다. + +반면 라이브러리는 A라는 트리 컴포넌트 뿐만이 아니라 B, C같이 여러 곳에서 읽고 쓰는 값에 사용한다. +여기서 또 나처럼 질문이 생길 수도 있다. 엥 그냥 값 관리면 context로 관리하는 것도 라이브러리로 하면 되는 거 아닌감? +모달로 예시를 들어보자. 나는 a라는 모달의 오픈 여부를 담아가고 싶다. 그걸 라이브러리로 관리하면 Modal이 닫혀도 상태가 남아있고, 앱 어디서든 접근 가능해져서 관심사 분리가 깨지는 것이다. +-> 왜? 라이브러리는 storge에 저장이 되는데, 그건 페이지를 닫거나 아예 값을 날려버리기 전까지는 남아있기 때문이다. (앱 생명주기 동안 살아있다는 뜻) +-> 근데 context는 Provider가 unmount되면 같이 사라진다. (모달이 닫히면 사라진다는 뜻) + +props drilling이 일어났을 때 유용한 것은 맞지만 context의 주 목적이 props drilling이 아니라는 것에 초점을 맞춰야 한다. +나처럼 props drilling에 초점을 맞췄다가 어? 그렴 라이브러리가 더 나은 거 아닌가? ㅇㅅㅇa 가 될 수도 있기 때문이다. +props drilling > 부수 효과. 목적이 아님!! + +--- + \_참고: [heropy.dev](https://heropy.dev), [React 공식 문서](https://ko.react.dev/learn/passing-data-deeply-with-context) From f51a26e7ce9fd1bf6775079f8559edd978c0d1ef Mon Sep 17 00:00:00 2001 From: za0012 Date: Fri, 3 Apr 2026 16:49:33 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20context=20=EA=B5=AC=ED=98=84.=20p?= =?UTF-8?q?rops=20drilling=20=EC=BD=94=EB=93=9C=EB=A5=BC=20context?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react/ts/src/pages/context/context.css | 269 +++++++++ .../src/pages/context/dashboard/dashboard.css | 533 ++++++++++++++++++ .../pages/context/dashboard/fix/dashboard.css | 533 ++++++++++++++++++ .../src/pages/context/dashboard/fix/index.tsx | 304 ++++++++++ .../ts/src/pages/context/dashboard/index.tsx | 320 +++++++++++ .../react/ts/src/pages/context/index.tsx | 220 ++++++++ 6 files changed, 2179 insertions(+) create mode 100644 log/workspace/react/ts/src/pages/context/context.css create mode 100644 log/workspace/react/ts/src/pages/context/dashboard/dashboard.css create mode 100644 log/workspace/react/ts/src/pages/context/dashboard/fix/dashboard.css create mode 100644 log/workspace/react/ts/src/pages/context/dashboard/fix/index.tsx create mode 100644 log/workspace/react/ts/src/pages/context/dashboard/index.tsx create mode 100644 log/workspace/react/ts/src/pages/context/index.tsx diff --git a/log/workspace/react/ts/src/pages/context/context.css b/log/workspace/react/ts/src/pages/context/context.css new file mode 100644 index 0000000..1fe86df --- /dev/null +++ b/log/workspace/react/ts/src/pages/context/context.css @@ -0,0 +1,269 @@ +/* context.css */ + +/* ============================================ + Reset & Base + ============================================ */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background-color: #0f0f13; + color: #e2e2e8; + line-height: 1.6; + min-height: 100vh; +} + +/* ============================================ + App + ============================================ */ +.app { + display: flex; + flex-direction: row; + align-items: center; + padding: 40px 24px; + min-height: 100vh; + gap: 50px; +} + +/* ============================================ + Page + ============================================ */ +.page { + width: 100%; + max-width: 720px; + background: #1a1a24; + border: 1px solid #2e2e3e; + border-radius: 16px; + padding: 32px; +} + +.page > h1 { + font-size: 22px; + font-weight: 600; + color: #c4b5fd; + margin-bottom: 24px; + padding-bottom: 12px; + border-bottom: 1px solid #2e2e3e; + letter-spacing: -0.3px; +} + +/* ============================================ + Section + ============================================ */ +.section { + background: #1f1f2e; + border: 1px solid #35354a; + border-radius: 12px; + padding: 24px; +} + +.section > h2 { + font-size: 18px; + font-weight: 500; + color: #a78bfa; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 8px; +} + +.section > h2::before { + content: ""; + display: inline-block; + width: 4px; + height: 18px; + background: #7c3aed; + border-radius: 2px; +} + +/* ============================================ + Panel + ============================================ */ +.panel { + background: #24243a; + border: 1px solid #3d3d56; + border-radius: 10px; + padding: 20px; +} + +.panel > h3 { + font-size: 15px; + font-weight: 500; + color: #fb923c; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.panel > h3::before { + content: ""; + display: inline-block; + width: 3px; + height: 15px; + background: #ea580c; + border-radius: 2px; +} + +/* ============================================ + Widget + ============================================ */ +.widget { + background: #2a2040; + border: 1px dashed #4a3a6e; + border-radius: 8px; + padding: 16px; +} + +/* ============================================ + DeepChild — 실제 사용처 + ============================================ */ +.deep-child { + background: #1e1230; + border: 1.5px solid #6d28d9; + border-radius: 10px; + padding: 20px 24px; + position: relative; + overflow: hidden; +} + +.deep-child::before { + content: "5단계 아래"; + position: absolute; + top: 10px; + right: 12px; + font-size: 11px; + color: #7c3aed; + background: #2e1065; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid #4c1d95; + letter-spacing: 0.3px; +} + +/* theme-dark */ +.deep-child.theme-dark { + background: #1a0f2e; + box-shadow: + 0 0 0 1px #6d28d9, + 0 4px 24px rgba(109, 40, 217, 0.15); +} + +/* theme-light */ +.deep-child.theme-light { + background: #f5f3ff; + color: #1e1230; + border-color: #7c3aed; +} + +.deep-child.theme-light p { + color: #3b0764; +} + +.deep-child.theme-light .user-name { + color: #6d28d9; +} + +.deep-child.theme-light .badge { + background: #ede9fe; + color: #5b21b6; + border-color: #c4b5fd; +} + +.deep-child.theme-light .locale-tag { + background: #f3e8ff; + color: #6d28d9; +} + +/* DeepChild 내부 요소들 */ +.deep-child p { + font-size: 15px; + color: #d1d5db; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 8px; +} + +.deep-child p:last-child { + margin-bottom: 0; +} + +.user-name { + font-weight: 600; + color: #c084fc; + font-size: 16px; +} + +.badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + border: 1px solid; +} + +.badge.admin { + background: #fef3c7; + color: #92400e; + border-color: #fcd34d; +} + +.badge.user { + background: #dbeafe; + color: #1e40af; + border-color: #93c5fd; +} + +.locale-tag { + display: inline-flex; + align-items: center; + gap: 4px; + background: #1e1b4b; + color: #a5b4fc; + border: 1px solid #3730a3; + padding: 2px 10px; + border-radius: 6px; + font-size: 12px; + font-family: "SF Mono", "Fira Code", monospace; +} + +/* ============================================ + Depth Indicator (선택적 시각 요소) + ============================================ */ +.depth-indicator { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.depth-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #3b3b52; + border: 1.5px solid #555570; + transition: background 0.2s; +} + +.depth-dot.active { + background: #7c3aed; + border-color: #a78bfa; + box-shadow: 0 0 6px rgba(124, 58, 237, 0.6); +} + +.depth-arrow { + font-size: 11px; + color: #555570; +} diff --git a/log/workspace/react/ts/src/pages/context/dashboard/dashboard.css b/log/workspace/react/ts/src/pages/context/dashboard/dashboard.css new file mode 100644 index 0000000..d459bff --- /dev/null +++ b/log/workspace/react/ts/src/pages/context/dashboard/dashboard.css @@ -0,0 +1,533 @@ +/* dashboard.css */ + +/* ============================================ + Tokens & Reset + ============================================ */ +:root { + --bg: #0c0c10; + --bg-2: #13131a; + --bg-3: #1a1a24; + --bg-4: #20202e; + --border: #ffffff0f; + --border-2: #ffffff18; + --text: #e8e8f0; + --text-2: #9898b0; + --text-3: #5a5a72; + --accent: #6d6af5; + --accent-2: #4d4abf; + --accent-glow: #6d6af530; + --green: #34d399; + --red: #f87171; + --gold: #fbbf24; + --gold-2: #d78502; + --radius: 10px; + --sidebar-w: 220px; + --font-mono: "SF Mono", "Fira Code", "Cascadia Code", monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--bg); + color: var(--text); + font-family: + "Pretendard", + "Apple SD Gothic Neo", + -apple-system, + sans-serif; + font-size: 14px; + line-height: 1.6; + min-height: 100vh; +} + +/* ============================================ + Logged-out screen + ============================================ */ +.logged-out { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + gap: 20px; +} + +.logged-out p { + font-size: 20px; + color: var(--text-2); +} + +.logged-out button { + padding: 10px 24px; + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--radius); + font-size: 14px; + cursor: pointer; + transition: background 0.15s; +} + +.logged-out button:hover { + background: var(--accent-2); +} + +/* ============================================ + Dashboard Layout + ============================================ */ +.dashboard-page { + min-height: 100vh; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +/* ============================================ + Sidebar + ============================================ */ +.sidebar { + width: var(--sidebar-w); + background: var(--bg-2); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + padding: 20px 0; + flex-shrink: 0; + position: fixed; + top: 0; + left: 0; + height: 100vh; +} + +.sidebar-logo { + display: flex; + align-items: center; + gap: 10px; + padding: 0 20px 20px; + border-bottom: 1px solid var(--border); + margin-bottom: 12px; +} + +.logo-mark { + font-size: 20px; + color: var(--accent); + line-height: 1; +} + +.logo-text { + font-size: 16px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text); +} + +/* ============================================ + Sidebar Menu + ============================================ */ +.sidebar-menu { + display: flex; + flex-direction: column; + flex: 1; +} + +.sidebar-menu ul { + list-style: none; + padding: 0 10px; + flex: 1; +} + +/* ============================================ + Sidebar Menu Item + ============================================ */ +.menu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + border-radius: 8px; + cursor: pointer; + position: relative; + color: var(--text-2); + transition: + background 0.12s, + color 0.12s; + margin-bottom: 2px; + user-select: none; +} + +.menu-item:hover { + background: var(--bg-3); + color: var(--text); +} + +.menu-item.active { + background: var(--accent-glow); + color: var(--text); +} + +.menu-item.locked { + opacity: 0.5; + cursor: not-allowed; +} + +.menu-icon { + font-size: 16px; + width: 20px; + text-align: center; + flex-shrink: 0; +} + +.menu-label { + font-size: 13.5px; + flex: 1; +} + +.menu-badge { + font-size: 10px; + font-weight: 600; + background: var(--gold); + color: #000; + padding: 1px 6px; + border-radius: 4px; + letter-spacing: 0.3px; +} + +.menu-active-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + flex-shrink: 0; +} + +/* ============================================ + User Panel (드디어 user 실제 사용처!) + ============================================ */ +.user-panel { + position: relative; + padding: 10px; + border-top: 1px solid var(--border); + margin-top: auto; +} + +.user-panel-trigger { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + background: none; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.12s; + color: var(--text); + text-align: left; +} + +.user-panel-trigger:hover { + background: var(--bg-3); +} + +.avatar { + width: 32px; + height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: #fff; + flex-shrink: 0; + overflow: hidden; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.user-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.user-name { + font-size: 13px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text); +} + +.user-plan { + font-size: 11px; + color: var(--gold); +} + +.chevron { + font-size: 18px; + color: var(--text-3); + transition: transform 0.2s; + flex-shrink: 0; + line-height: 1; +} + +.chevron.open { + transform: rotate(90deg); +} + +/* User dropdown menu */ +.user-menu { + position: absolute; + bottom: calc(100% + 4px); + left: 10px; + right: 10px; + background: var(--bg-3); + border: 1px solid var(--border-2); + border-radius: 10px; + padding: 6px; + box-shadow: 0 -8px 32px #00000050; + animation: slideUp 0.15s ease; + z-index: 10; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.user-menu-email { + font-size: 11px; + color: var(--text-3); + padding: 4px 8px 8px; + font-family: var(--font-mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-menu-divider { + border: none; + border-top: 1px solid var(--border); + margin: 4px 0; +} + +.user-menu-item { + display: block; + width: 100%; + padding: 7px 10px; + background: none; + border: none; + border-radius: 6px; + text-align: left; + font-size: 13px; + color: var(--text-2); + cursor: pointer; + transition: + background 0.1s, + color 0.1s; +} + +.user-menu-item:hover { + background: var(--bg-4); + color: var(--text); +} + +.user-menu-item.danger { + color: var(--red); +} + +.user-menu-item.danger:hover { + background: #f8717115; + color: var(--red); +} + +/* ============================================ + Main Content + ============================================ */ +.main { + flex: 1; + margin-left: var(--sidebar-w); + padding: 32px; + min-height: 100vh; +} + +.main-content { + max-width: 900px; +} + +.content-header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 28px; +} + +.content-header h1 { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text); +} + +.content-date { + font-size: 13px; + color: var(--text-3); +} + +/* ============================================ + Stats Grid + ============================================ */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; + margin-bottom: 28px; +} + +.stat-card { + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px 20px; + display: flex; + flex-direction: column; + gap: 6px; + transition: border-color 0.2s; +} + +.stat-card:hover { + border-color: var(--border-2); +} + +.stat-label { + font-size: 12px; + color: var(--text-3); + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.stat-value { + font-size: 26px; + font-weight: 700; + letter-spacing: -1px; + color: var(--text); + line-height: 1.1; +} + +.stat-delta { + font-size: 12px; + font-weight: 500; +} + +.stat-delta.up { + color: var(--green); +} +.stat-delta.down { + color: var(--red); +} + +/* ============================================ + Drill Callout + ============================================ */ +.drill-callout { + display: flex; + gap: 14px; + background: #1e1a0e; + border: 1px solid #fbbf2430; + border-left: 3px solid var(--gold); + border-radius: var(--radius); + padding: 16px 20px; + margin-top: 8px; +} + +.callout-icon { + font-size: 18px; + color: var(--gold); + flex-shrink: 0; + margin-top: 1px; +} + +.drill-callout strong { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--gold); + margin-bottom: 6px; +} + +.drill-callout p { + font-size: 13px; + color: var(--text-2); + line-height: 1.7; +} + +.drill-callout code { + font-family: var(--font-mono); + font-size: 12px; + background: #ffffff0d; + padding: 1px 5px; + border-radius: 4px; + color: var(--accent); +} + +.drill-callout em { + color: var(--red); + font-style: normal; + font-weight: 600; +} + +.move-route { + padding: 8px 14px; + background: var(--gold); + color: #000; + border: none; + border-radius: var(--radius); + font-size: 14px; + cursor: pointer; + transition: background 0.15s; + margin-top: 10px; +} + +.move-route:hover { + background: var(--gold-2); +} + +/* ============================================ + Responsive + ============================================ */ +@media (max-width: 768px) { + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .stats-grid { + grid-template-columns: 1fr; + } + .main { + padding: 20px; + } +} diff --git a/log/workspace/react/ts/src/pages/context/dashboard/fix/dashboard.css b/log/workspace/react/ts/src/pages/context/dashboard/fix/dashboard.css new file mode 100644 index 0000000..d459bff --- /dev/null +++ b/log/workspace/react/ts/src/pages/context/dashboard/fix/dashboard.css @@ -0,0 +1,533 @@ +/* dashboard.css */ + +/* ============================================ + Tokens & Reset + ============================================ */ +:root { + --bg: #0c0c10; + --bg-2: #13131a; + --bg-3: #1a1a24; + --bg-4: #20202e; + --border: #ffffff0f; + --border-2: #ffffff18; + --text: #e8e8f0; + --text-2: #9898b0; + --text-3: #5a5a72; + --accent: #6d6af5; + --accent-2: #4d4abf; + --accent-glow: #6d6af530; + --green: #34d399; + --red: #f87171; + --gold: #fbbf24; + --gold-2: #d78502; + --radius: 10px; + --sidebar-w: 220px; + --font-mono: "SF Mono", "Fira Code", "Cascadia Code", monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--bg); + color: var(--text); + font-family: + "Pretendard", + "Apple SD Gothic Neo", + -apple-system, + sans-serif; + font-size: 14px; + line-height: 1.6; + min-height: 100vh; +} + +/* ============================================ + Logged-out screen + ============================================ */ +.logged-out { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + gap: 20px; +} + +.logged-out p { + font-size: 20px; + color: var(--text-2); +} + +.logged-out button { + padding: 10px 24px; + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--radius); + font-size: 14px; + cursor: pointer; + transition: background 0.15s; +} + +.logged-out button:hover { + background: var(--accent-2); +} + +/* ============================================ + Dashboard Layout + ============================================ */ +.dashboard-page { + min-height: 100vh; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +/* ============================================ + Sidebar + ============================================ */ +.sidebar { + width: var(--sidebar-w); + background: var(--bg-2); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + padding: 20px 0; + flex-shrink: 0; + position: fixed; + top: 0; + left: 0; + height: 100vh; +} + +.sidebar-logo { + display: flex; + align-items: center; + gap: 10px; + padding: 0 20px 20px; + border-bottom: 1px solid var(--border); + margin-bottom: 12px; +} + +.logo-mark { + font-size: 20px; + color: var(--accent); + line-height: 1; +} + +.logo-text { + font-size: 16px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text); +} + +/* ============================================ + Sidebar Menu + ============================================ */ +.sidebar-menu { + display: flex; + flex-direction: column; + flex: 1; +} + +.sidebar-menu ul { + list-style: none; + padding: 0 10px; + flex: 1; +} + +/* ============================================ + Sidebar Menu Item + ============================================ */ +.menu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + border-radius: 8px; + cursor: pointer; + position: relative; + color: var(--text-2); + transition: + background 0.12s, + color 0.12s; + margin-bottom: 2px; + user-select: none; +} + +.menu-item:hover { + background: var(--bg-3); + color: var(--text); +} + +.menu-item.active { + background: var(--accent-glow); + color: var(--text); +} + +.menu-item.locked { + opacity: 0.5; + cursor: not-allowed; +} + +.menu-icon { + font-size: 16px; + width: 20px; + text-align: center; + flex-shrink: 0; +} + +.menu-label { + font-size: 13.5px; + flex: 1; +} + +.menu-badge { + font-size: 10px; + font-weight: 600; + background: var(--gold); + color: #000; + padding: 1px 6px; + border-radius: 4px; + letter-spacing: 0.3px; +} + +.menu-active-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + flex-shrink: 0; +} + +/* ============================================ + User Panel (드디어 user 실제 사용처!) + ============================================ */ +.user-panel { + position: relative; + padding: 10px; + border-top: 1px solid var(--border); + margin-top: auto; +} + +.user-panel-trigger { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + background: none; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.12s; + color: var(--text); + text-align: left; +} + +.user-panel-trigger:hover { + background: var(--bg-3); +} + +.avatar { + width: 32px; + height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: #fff; + flex-shrink: 0; + overflow: hidden; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.user-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.user-name { + font-size: 13px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text); +} + +.user-plan { + font-size: 11px; + color: var(--gold); +} + +.chevron { + font-size: 18px; + color: var(--text-3); + transition: transform 0.2s; + flex-shrink: 0; + line-height: 1; +} + +.chevron.open { + transform: rotate(90deg); +} + +/* User dropdown menu */ +.user-menu { + position: absolute; + bottom: calc(100% + 4px); + left: 10px; + right: 10px; + background: var(--bg-3); + border: 1px solid var(--border-2); + border-radius: 10px; + padding: 6px; + box-shadow: 0 -8px 32px #00000050; + animation: slideUp 0.15s ease; + z-index: 10; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.user-menu-email { + font-size: 11px; + color: var(--text-3); + padding: 4px 8px 8px; + font-family: var(--font-mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-menu-divider { + border: none; + border-top: 1px solid var(--border); + margin: 4px 0; +} + +.user-menu-item { + display: block; + width: 100%; + padding: 7px 10px; + background: none; + border: none; + border-radius: 6px; + text-align: left; + font-size: 13px; + color: var(--text-2); + cursor: pointer; + transition: + background 0.1s, + color 0.1s; +} + +.user-menu-item:hover { + background: var(--bg-4); + color: var(--text); +} + +.user-menu-item.danger { + color: var(--red); +} + +.user-menu-item.danger:hover { + background: #f8717115; + color: var(--red); +} + +/* ============================================ + Main Content + ============================================ */ +.main { + flex: 1; + margin-left: var(--sidebar-w); + padding: 32px; + min-height: 100vh; +} + +.main-content { + max-width: 900px; +} + +.content-header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 28px; +} + +.content-header h1 { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text); +} + +.content-date { + font-size: 13px; + color: var(--text-3); +} + +/* ============================================ + Stats Grid + ============================================ */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; + margin-bottom: 28px; +} + +.stat-card { + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px 20px; + display: flex; + flex-direction: column; + gap: 6px; + transition: border-color 0.2s; +} + +.stat-card:hover { + border-color: var(--border-2); +} + +.stat-label { + font-size: 12px; + color: var(--text-3); + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.stat-value { + font-size: 26px; + font-weight: 700; + letter-spacing: -1px; + color: var(--text); + line-height: 1.1; +} + +.stat-delta { + font-size: 12px; + font-weight: 500; +} + +.stat-delta.up { + color: var(--green); +} +.stat-delta.down { + color: var(--red); +} + +/* ============================================ + Drill Callout + ============================================ */ +.drill-callout { + display: flex; + gap: 14px; + background: #1e1a0e; + border: 1px solid #fbbf2430; + border-left: 3px solid var(--gold); + border-radius: var(--radius); + padding: 16px 20px; + margin-top: 8px; +} + +.callout-icon { + font-size: 18px; + color: var(--gold); + flex-shrink: 0; + margin-top: 1px; +} + +.drill-callout strong { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--gold); + margin-bottom: 6px; +} + +.drill-callout p { + font-size: 13px; + color: var(--text-2); + line-height: 1.7; +} + +.drill-callout code { + font-family: var(--font-mono); + font-size: 12px; + background: #ffffff0d; + padding: 1px 5px; + border-radius: 4px; + color: var(--accent); +} + +.drill-callout em { + color: var(--red); + font-style: normal; + font-weight: 600; +} + +.move-route { + padding: 8px 14px; + background: var(--gold); + color: #000; + border: none; + border-radius: var(--radius); + font-size: 14px; + cursor: pointer; + transition: background 0.15s; + margin-top: 10px; +} + +.move-route:hover { + background: var(--gold-2); +} + +/* ============================================ + Responsive + ============================================ */ +@media (max-width: 768px) { + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .stats-grid { + grid-template-columns: 1fr; + } + .main { + padding: 20px; + } +} diff --git a/log/workspace/react/ts/src/pages/context/dashboard/fix/index.tsx b/log/workspace/react/ts/src/pages/context/dashboard/fix/index.tsx new file mode 100644 index 0000000..22da5ad --- /dev/null +++ b/log/workspace/react/ts/src/pages/context/dashboard/fix/index.tsx @@ -0,0 +1,304 @@ +// PropDrillingDashboard.tsx +// 실제 SaaS 대시보드에서 흔히 발생하는 prop drilling 지옥 +// user 정보가 App → DashboardPage → DashboardLayout → Sidebar → SidebarMenu → SidebarMenuItem 까지 뚫고 내려감 + +import { createContext, useContext, useState } from "react"; +import "./dashboard.css"; +import { Link } from "react-router-dom"; + +// ============================================ +// Types +// ============================================ +type Plan = "free" | "pro" | "enterprise"; + +interface User { + id: string; + name: string; + email: string; + avatarUrl: string; + plan: Plan; +} + +interface contextFix { + user: User; + onLogout: () => void; +} + +const ContextFix = createContext({ + user: { id: "", name: "", email: "", avatarUrl: "", plan: "free" }, + onLogout: function () {}, +}); +// ============================================ +// App.tsx — 유저 상태 + 로그아웃 로직 보유 +// ============================================ +export default function App() { + const [user, setUser] = useState({ + id: "u_1a2b3c", + name: "김지수", + email: "jisu@company.io", + avatarUrl: + "https://i.pinimg.com/236x/3a/52/69/3a52694b8f231960a2625f838be6ea89.jpg", + plan: "pro", + }); + + const [loggedIn, setLoggedIn] = useState(true); + + function handleLogout() { + setLoggedIn(false); + setUser({ id: "", name: "", email: "", avatarUrl: "", plan: "free" }); + } + + if (!loggedIn) { + return ( +
+

로그아웃됐습니다 👋

+ +
+ ); + } + + // 😤 user랑 onLogout을 DashboardPage에 넘기기 시작... + return ( + + ; + + ); +} + +// ============================================ +// DashboardPage.tsx +// user, onLogout 받아서 → DashboardLayout에 넘김 +// 자기 자신은 그냥 페이지 타이틀만 씀 +// ============================================ + +function DashboardPage() { + return ( +
+ {/* user는 여기서 안 쓰고, 그대로 Layout에 꽂아줌 */} + + + +
+ ); +} + +// ============================================ +// DashboardLayout.tsx +// user, onLogout 받아서 → Sidebar에 넘김 +// 자기 자신은 레이아웃 구조만 담당 +// ============================================ +interface DashboardLayoutProps { + children: React.ReactNode; +} + +function DashboardLayout({ children }: DashboardLayoutProps) { + return ( +
+ {/* 또 그대로 전달... */} + +
{children}
+
+ ); +} + +// ============================================ +// Sidebar.tsx +// user, onLogout 받아서 → SidebarMenu에 넘김 +// 자기 자신은 사이드바 껍데기만 담당 +// ============================================ + +function Sidebar() { + return ( + + ); +} + +// ============================================ +// SidebarMenu.tsx +// user, onLogout 받아서 → 각 SidebarMenuItem에 넘김 +// 자기 자신은 메뉴 목록 구조만 담당 +// ============================================ +const MENU_ITEMS = [ + { id: "overview", icon: "⊞", label: "Overview", isActive: true }, + { id: "analytics", icon: "↗", label: "Analytics", isActive: false }, + { id: "files", icon: "⊟", label: "Files", isActive: false }, + { id: "settings", icon: "⊙", label: "Settings", isActive: false }, +]; + +function SidebarMenu() { + return ( + + ); +} + +// ============================================ +// SidebarMenuItem.tsx +// user, onLogout 받지만... 아이콘/라벨 메뉴 아이템인데 +// 굳이 user 정보가 왜 여기까지 내려와야 하지? 😑 +// 결국 그냥 isActive 뱃지 표시할 때 plan 체크용으로 씀 +// ============================================ +interface SidebarMenuItemProps { + icon: string; + label: string; + isActive: boolean; +} + +function SidebarMenuItem({ icon, label, isActive }: SidebarMenuItemProps) { + const context = useContext(ContextFix); + const isLocked = label === "Analytics" && context.user.plan === "free"; + + return ( +
  • + {icon} + {label} + {isLocked && Pro} + {isActive && } +
  • + ); +} + +// ============================================ +// SidebarUserPanel.tsx +// 드디어! user + onLogout 실제로 쓰는 컴포넌트 +// ============================================ +const PLAN_LABEL: Record = { + free: "Free ♪", + pro: "Pro ✦", + enterprise: "Enterprise ◍", +}; + +function SidebarUserPanel() { + const context = useContext(ContextFix); + const [showMenu, setShowMenu] = useState(false); + const initials = context.user.name + .split(" ") + .map((n) => n[0]) + .join("") + .slice(0, 2); + + return ( +
    + + + {showMenu && ( +
    +
    {context.user.email}
    +
    + + +
    + +
    + )} +
    + ); +} + +// ============================================ +// MainContent.tsx — props 없음, 그냥 콘텐츠 +// ============================================ +function MainContent() { + return ( +
    +
    +

    Overview

    + 2025년 4월 +
    + +
    + {[ + { label: "총 방문자", value: "24,821", delta: "+12.4%" }, + { label: "전환율", value: "3.6%", delta: "+0.8%" }, + { label: "월 매출", value: "₩4.2M", delta: "+21%" }, + { label: "활성 유저", value: "1,204", delta: "-2.1%" }, + ].map((stat) => ( +
    + {stat.label} + {stat.value} + + {stat.delta} + +
    + ))} +
    + +
    + +
    + Prop Drilling 경고 해결 +

    + useronLogout은{" "} + + App → DashboardPage → DashboardLayout → Sidebar → SidebarMenu → + SidebarMenuItem / SidebarUserPanel + + 까지 6단계를 뚫고 내려갔습니다. +
    + 중간 컴포넌트 4개는 이 props를 전달만 했을 뿐 사용하지 + 않았습니다. +

    + + + +
    +
    +
    + ); +} diff --git a/log/workspace/react/ts/src/pages/context/dashboard/index.tsx b/log/workspace/react/ts/src/pages/context/dashboard/index.tsx new file mode 100644 index 0000000..ad74edf --- /dev/null +++ b/log/workspace/react/ts/src/pages/context/dashboard/index.tsx @@ -0,0 +1,320 @@ +// PropDrillingDashboard.tsx +// 실제 SaaS 대시보드에서 흔히 발생하는 prop drilling 지옥 +// user 정보가 App → DashboardPage → DashboardLayout → Sidebar → SidebarMenu → SidebarMenuItem 까지 뚫고 내려감 + +import { useState } from "react"; +import "./dashboard.css"; +import { Link } from "react-router-dom"; + +// ============================================ +// Types +// ============================================ +type Plan = "free" | "pro" | "enterprise"; + +interface User { + id: string; + name: string; + email: string; + avatarUrl: string; + plan: Plan; +} + +// ============================================ +// App.tsx — 유저 상태 + 로그아웃 로직 보유 +// ============================================ +export default function App() { + const [user, setUser] = useState({ + id: "u_1a2b3c", + name: "김지수", + email: "jisu@company.io", + avatarUrl: + "https://i.pinimg.com/236x/3a/52/69/3a52694b8f231960a2625f838be6ea89.jpg", + plan: "pro", + }); + + const [loggedIn, setLoggedIn] = useState(true); + + function handleLogout() { + setLoggedIn(false); + setUser({ id: "", name: "", email: "", avatarUrl: "", plan: "free" }); + } + + if (!loggedIn) { + return ( +
    +

    로그아웃됐습니다 👋

    + +
    + ); + } + + // 😤 user랑 onLogout을 DashboardPage에 넘기기 시작... + return ; +} + +// ============================================ +// DashboardPage.tsx +// user, onLogout 받아서 → DashboardLayout에 넘김 +// 자기 자신은 그냥 페이지 타이틀만 씀 +// ============================================ +interface DashboardPageProps { + user: User; // ← 직접 안 씀 + onLogout: () => void; // ← 직접 안 씀 +} + +function DashboardPage({ user, onLogout }: DashboardPageProps) { + return ( +
    + {/* user는 여기서 안 쓰고, 그대로 Layout에 꽂아줌 */} + + + +
    + ); +} + +// ============================================ +// DashboardLayout.tsx +// user, onLogout 받아서 → Sidebar에 넘김 +// 자기 자신은 레이아웃 구조만 담당 +// ============================================ +interface DashboardLayoutProps { + user: User; // ← 직접 안 씀 + onLogout: () => void; // ← 직접 안 씀 + children: React.ReactNode; +} + +function DashboardLayout({ user, onLogout, children }: DashboardLayoutProps) { + return ( +
    + {/* 또 그대로 전달... */} + +
    {children}
    +
    + ); +} + +// ============================================ +// Sidebar.tsx +// user, onLogout 받아서 → SidebarMenu에 넘김 +// 자기 자신은 사이드바 껍데기만 담당 +// ============================================ +interface SidebarProps { + user: User; // ← 직접 안 씀 + onLogout: () => void; // ← 직접 안 씀 +} + +function Sidebar({ user, onLogout }: SidebarProps) { + return ( + + ); +} + +// ============================================ +// SidebarMenu.tsx +// user, onLogout 받아서 → 각 SidebarMenuItem에 넘김 +// 자기 자신은 메뉴 목록 구조만 담당 +// ============================================ +interface SidebarMenuProps { + user: User; // ← 직접 안 씀 (아이템에 넘김) + onLogout: () => void; // ← 직접 안 씀 (아이템에 넘김) +} + +const MENU_ITEMS = [ + { id: "overview", icon: "⊞", label: "Overview", isActive: true }, + { id: "analytics", icon: "↗", label: "Analytics", isActive: false }, + { id: "files", icon: "⊟", label: "Files", isActive: false }, + { id: "settings", icon: "⊙", label: "Settings", isActive: false }, +]; + +function SidebarMenu({ user, onLogout }: SidebarMenuProps) { + return ( + + ); +} + +// ============================================ +// SidebarMenuItem.tsx +// user, onLogout 받지만... 아이콘/라벨 메뉴 아이템인데 +// 굳이 user 정보가 왜 여기까지 내려와야 하지? 😑 +// 결국 그냥 isActive 뱃지 표시할 때 plan 체크용으로 씀 +// ============================================ +interface SidebarMenuItemProps { + icon: string; + label: string; + isActive: boolean; + user: User; // ← plan 체크 때문에 받긴 함... + onLogout: () => void; // ← 여기선 안 씀. 근데 받아야 함 (SidebarUserPanel에 안 내려가니까) +} + +function SidebarMenuItem({ + icon, + label, + isActive, + user, + onLogout, // 여기선 쓰지도 않는데 받아야 했던 불쌍한 prop +}: SidebarMenuItemProps) { + const isLocked = label === "Analytics" && user.plan === "free"; + + return ( +
  • + {icon} + {label} + {isLocked && Pro} + {isActive && } +
  • + ); +} + +// ============================================ +// SidebarUserPanel.tsx +// 드디어! user + onLogout 실제로 쓰는 컴포넌트 +// ============================================ +interface SidebarUserPanelProps { + user: User; + onLogout: () => void; +} + +const PLAN_LABEL: Record = { + free: "Free ♪", + pro: "Pro ✦", + enterprise: "Enterprise ◍", +}; + +function SidebarUserPanel({ user, onLogout }: SidebarUserPanelProps) { + const [showMenu, setShowMenu] = useState(false); + const initials = user.name + .split(" ") + .map((n) => n[0]) + .join("") + .slice(0, 2); + + return ( +
    + + + {showMenu && ( +
    +
    {user.email}
    +
    + + +
    + +
    + )} +
    + ); +} + +// ============================================ +// MainContent.tsx — props 없음, 그냥 콘텐츠 +// ============================================ +function MainContent() { + return ( +
    +
    +

    Overview

    + 2025년 4월 +
    + +
    + {[ + { label: "총 방문자", value: "24,821", delta: "+12.4%" }, + { label: "전환율", value: "3.6%", delta: "+0.8%" }, + { label: "월 매출", value: "₩4.2M", delta: "+21%" }, + { label: "활성 유저", value: "1,204", delta: "-2.1%" }, + ].map((stat) => ( +
    + {stat.label} + {stat.value} + + {stat.delta} + +
    + ))} +
    + +
    + +
    + Prop Drilling 경고 +

    + useronLogout은{" "} + + App → DashboardPage → DashboardLayout → Sidebar → SidebarMenu → + SidebarMenuItem / SidebarUserPanel + + 까지 6단계를 뚫고 내려갔습니다. +
    + 중간 컴포넌트 4개는 이 props를 전달만 했을 뿐 사용하지 + 않았습니다. +

    + + + +
    +
    +
    + ); +} diff --git a/log/workspace/react/ts/src/pages/context/index.tsx b/log/workspace/react/ts/src/pages/context/index.tsx new file mode 100644 index 0000000..743d996 --- /dev/null +++ b/log/workspace/react/ts/src/pages/context/index.tsx @@ -0,0 +1,220 @@ +import { createContext, useContext } from "react"; +import "./context.css"; + +// export function index() { +// const ProviderTest = createContext(""); +// return ; +// } + +// types.ts +interface UserInfo { + name: string; + email: string; + role: "admin" | "user"; +} + +interface UserInfoFix { + name: string; + email: string; + role: "admin" | "user"; + theme: string; + locale: string; +} + +// ============================================ +// App.tsx — 데이터 원천 +// ============================================ +const ProviderTest = createContext({ + name: "", + email: "", + role: "admin", + theme: "", + locale: "", +}); + +function index() { + const userInfo: UserInfo = { + name: "김철수", + email: "kim@example.com", + role: "admin", + }; + const theme = "dark"; + const locale = "ko-KR"; + + return ( +
    + + + + +
    + ); +} + +// ============================================ +// Page.tsx — 쓰지도 않으면서 전달만 함 😑 +// ============================================ +interface PageProps { + userInfo: UserInfo; + theme: string; + locale: string; +} + +function Page({ userInfo, theme, locale }: PageProps) { + return ( +
    +

    페이지

    + {/* userInfo, theme, locale 하나도 안 씀 */} +
    +
    + ); +} + +function PageFix() { + return ( +
    +

    페이지 context 사용

    + {/* userInfo, theme, locale 하나도 안 씀 */} + +
    + ); +} + +// ============================================ +// Section.tsx — 마찬가지로 그냥 통과 👆 +// ============================================ +interface SectionProps { + userInfo: UserInfo; + theme: string; + locale: string; +} + +function Section({ userInfo, theme, locale }: SectionProps) { + return ( +
    +

    섹션

    + {/* 이것도 안 씀 ㅎ */} + +
    + ); +} + +function SectionFix() { + return ( +
    +

    섹션

    + {/* 이것도 안 씀 ㅎ */} + +
    + ); +} + +// ============================================ +// Panel.tsx — 역시 패스 🤦 +// ============================================ +interface PanelProps { + userInfo: UserInfo; + theme: string; + locale: string; +} + +function Panel({ userInfo, theme, locale }: PanelProps) { + return ( +
    +

    패널

    + +
    + ); +} + +function PanelFix() { + return ( +
    +

    패널

    + +
    + ); +} + +// ============================================ +// Widget.tsx — 또 패스 😭 +// ============================================ +interface WidgetProps { + userInfo: UserInfo; + theme: string; + locale: string; +} + +function Widget({ userInfo, theme, locale }: WidgetProps) { + return ( +
    + +
    + ); +} + +function WidgetFix() { + return ( +
    + +
    + ); +} + +// ============================================ +// DeepChild.tsx — 드디어!!!! 여기서 씀 🎉 +// ============================================ +interface DeepChildProps { + userInfo: UserInfo; + theme: string; + locale: string; +} + +function DeepChild({ userInfo, theme, locale }: DeepChildProps) { + return ( +
    +

    + 안녕하세요, {userInfo.name}님! +

    +

    + 권한: + + {userInfo.role === "admin" ? "관리자" : "일반 사용자"} + +

    +

    + 언어 설정: {locale} +

    +
    + ); +} + +function DeepChildFix() { + const userInfo = useContext(ProviderTest); + return ( +
    +

    + 안녕하세요, {userInfo.name}님! +

    +

    + 권한: + + {userInfo.role === "admin" ? "관리자" : "일반 사용자"} + +

    +

    + 언어 설정: {userInfo.locale} +

    +
    + ); +} + +export default index; From 902333b2af968cce00655ee9791f0cff3427c3f9 Mon Sep 17 00:00:00 2001 From: za0012 Date: Fri, 3 Apr 2026 16:50:18 +0900 Subject: [PATCH 14/14] =?UTF-8?q?chore:=20=EB=B2=94=EC=9C=84=EA=B0=80=20?= =?UTF-8?q?=EC=BB=A4=EC=A0=B8=20=ED=8C=8C=EC=9D=BC=EC=9D=98=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EB=A5=BC=20=EC=9D=B4=EB=8F=99=EC=8B=9C=EC=BC=B0?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log/data/kong/method/react/{ => useContext}/context.md | 0 log/data/kong/method/react/{ => useContext}/provider.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename log/data/kong/method/react/{ => useContext}/context.md (100%) rename log/data/kong/method/react/{ => useContext}/provider.md (100%) diff --git a/log/data/kong/method/react/context.md b/log/data/kong/method/react/useContext/context.md similarity index 100% rename from log/data/kong/method/react/context.md rename to log/data/kong/method/react/useContext/context.md diff --git a/log/data/kong/method/react/provider.md b/log/data/kong/method/react/useContext/provider.md similarity index 100% rename from log/data/kong/method/react/provider.md rename to log/data/kong/method/react/useContext/provider.md