From 2ab42b6e6bfb5ec90948100aa5c084cbfc1d2ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gromit=20=28=EC=A0=84=EB=AF=BC=EC=9E=AC=29?= <64779472+ssi02014@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:22:38 +0900 Subject: [PATCH] =?UTF-8?q?fix(react):=20ClientGate,=20IfElse,=20When,=20M?= =?UTF-8?q?ounted=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/hot-rabbits-matter.md | 5 +++ docs/docs/react/components/ClientGate.mdx | 20 +++++++---- docs/docs/react/components/IfElse.mdx | 24 +++++++------ docs/docs/react/components/When.mdx | 7 ++-- .../react/src/components/ClientGate/index.tsx | 34 ++++++++++++++----- .../src/components/IfElse/IfElse.spec.tsx | 32 ++++++++--------- .../react/src/components/IfElse/index.tsx | 30 +++++++++++----- .../react/src/components/Mounted/index.tsx | 23 +++++++++---- .../react/src/components/When/When.spec.tsx | 12 +++---- packages/react/src/components/When/index.tsx | 30 +++++++++++++--- 10 files changed, 145 insertions(+), 72 deletions(-) create mode 100644 .changeset/hot-rabbits-matter.md diff --git a/.changeset/hot-rabbits-matter.md b/.changeset/hot-rabbits-matter.md new file mode 100644 index 000000000..6e06a8c84 --- /dev/null +++ b/.changeset/hot-rabbits-matter.md @@ -0,0 +1,5 @@ +--- +'@modern-kit/react': patch +--- + +fix(react): ClientGate, IfElse, When, Mounted 인터페이스 개선 - @ssi02014 diff --git a/docs/docs/react/components/ClientGate.mdx b/docs/docs/react/components/ClientGate.mdx index 46b0fefc4..e3068ec47 100644 --- a/docs/docs/react/components/ClientGate.mdx +++ b/docs/docs/react/components/ClientGate.mdx @@ -3,9 +3,18 @@ import { ClientGate } from '@modern-kit/react'; # ClientGate -Client Side에서는 children을, Server Side에서는 fallback component를 보여주는 컴포넌트입니다. +`ClientGate`는 렌더링 환경에 따라 다른 컨텐츠를 보여주는 컴포넌트입니다: +- Client Side: `children` 컴포넌트를 렌더링 +- Server Side: `fallback` 컴포넌트를 렌더링 -Pure CSR 환경에서 렌더시에는 첫 라이프 사이클이 수행되어 mount가 완료되기 전부터 해당 component를 보여줍니다. +`CSR(Client-Side Rendering)` 환경에서는 컴포넌트가 마운트되기 전부터 children이 렌더링됩니다. + +일반적인 `useEffect` 사용하여 클라이언트 사이드 렌더링을 감지할 경우, 다음과 같은 문제가 발생할 수 있습니다: +- 초기 렌더링에서 fallback이 표시됨 +- `useEffect` 실행 후 children으로 리렌더링되는 `이중 렌더링` 발생 + +`useSyncExternalStore`를 사용하여 서버와 클라이언트 간의 hydration mismatch를 방지합니다. +- https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store#usesyncexternalstore
@@ -16,12 +25,11 @@ Pure CSR 환경에서 렌더시에는 첫 라이프 사이클이 수행되어 mo ```ts title="typescript" interface ClientGateProps { - fallback?: JSX.Element; + children: React.ReactNode; + fallback?: React.ReactNode; } -const ClientGate: ({ - fallback = <> -}: PropsWithChildren) => JSX.Element; +function ClientGate({ fallback, children }: ClientGateProps): JSX.Element; ``` ## Usage diff --git a/docs/docs/react/components/IfElse.mdx b/docs/docs/react/components/IfElse.mdx index 50143262c..90283bee2 100644 --- a/docs/docs/react/components/IfElse.mdx +++ b/docs/docs/react/components/IfElse.mdx @@ -4,8 +4,8 @@ import { IfElse } from '@modern-kit/react'; # IfElse `IfElse` 컴포넌트는 주어진 조건에 따라 두 가지 컴포넌트 중 하나를 렌더링하는 간단한 도구입니다. -이 컴포넌트는 **condition**이라는 property를 통해 조건을 지정하고, 조건이 참(`true`)이면 `trueComponent`를, -거짓(`false`)이면 `falseComponent`를 렌더링합니다. +이 컴포넌트는 **condition**이라는 property를 통해 조건을 지정하고, 조건이 참(`true`)이면 `truthyComponent`를, +거짓(`false`)이면 `falsyComponent`를 렌더링합니다. **condition** property는 단순한 `boolean`값 뿐만 아니라, `boolean`값을 반환하는 함수도 허용됩니다. 이 경우, 컴포넌트는 해당 함수를 호출하여 조건을 평가합니다. @@ -20,12 +20,16 @@ import { IfElse } from '@modern-kit/react'; type Condition = boolean | (() => boolean); interface IfElseProps { - condition: Condition; - trueComponent: React.ReactNode; - falseComponent: React.ReactNode; + condition: Condition; + truthyComponent: React.ReactNode; + falsyComponent: React.ReactNode; } -const IfElse = ({ condition, trueComponent, falseComponent }: IfElseProps) => JSX.Element; +const IfElse: ({ + condition, + truthyComponent, + falsyComponent, +}: IfElseProps) => JSX.Element; ``` ## Usage @@ -39,8 +43,8 @@ const Example = () => { true component} - falseComponent={

false component

} + truthyComponent={

true component

} + falsyComponent={

false component

} /> ); @@ -55,8 +59,8 @@ export const Example = () => { true component} - falseComponent={

false component

} + truthyComponent={

true component

} + falsyComponent={

false component

} /> ); diff --git a/docs/docs/react/components/When.mdx b/docs/docs/react/components/When.mdx index 8ae1742a4..1d132a1b1 100644 --- a/docs/docs/react/components/When.mdx +++ b/docs/docs/react/components/When.mdx @@ -17,15 +17,12 @@ condition prop으로 `boolean을 반환하는 함수`도 허용됩니다. type Condition = boolean | (() => boolean); interface WhenProps { + children: React.ReactNode; condition: Condition; fallback?: React.ReactNode; } -const When: ({ - children, - condition, - fallback, -}: PropsWithChildren) => JSX.Element; +const When: ({ children, condition, fallback }: WhenProps) => JSX.Element; ``` ## Usage diff --git a/packages/react/src/components/ClientGate/index.tsx b/packages/react/src/components/ClientGate/index.tsx index 1511f35dd..9fa826e0c 100644 --- a/packages/react/src/components/ClientGate/index.tsx +++ b/packages/react/src/components/ClientGate/index.tsx @@ -1,9 +1,9 @@ -import { PropsWithChildren, ReactNode, useSyncExternalStore } from 'react'; - +import { useSyncExternalStore } from 'react'; import { noop } from '@modern-kit/utils'; interface ClientGateProps { - fallback?: JSX.Element; + children: React.ReactNode; + fallback?: React.ReactNode; } const subscribe = () => noop; @@ -11,22 +11,38 @@ const getSnapshot = () => false; const getServerSnapshot = () => true; /** - * @description Client Side에서는 children을, Server Side에서는 fallback component를 보여주는 컴포넌트입니다. + * @description `ClientGate`는 렌더링 환경에 따라 다른 컨텐츠를 보여주는 컴포넌트입니다: + * - Client Side: `children` 컴포넌트를 렌더링 + * - Server Side: `fallback` 컴포넌트를 렌더링 + * + * `CSR(Client-Side Rendering)` 환경에서는 컴포넌트가 마운트되기 전부터 children이 렌더링됩니다. + * + * 일반적인 `useEffect` 사용하여 클라이언트 사이드 렌더링을 감지할 경우, 다음과 같은 문제가 발생할 수 있습니다: + * - 초기 렌더링에서 fallback이 표시됨 + * - `useEffect` 실행 후 children으로 리렌더링되는 `이중 렌더링` 발생 + * + * `useSyncExternalStore`를 사용하여 서버와 클라이언트 간의 hydration mismatch를 방지합니다. + * @see https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store#usesyncexternalstore * * @param {ClientGateProps} props - 컴포넌트의 속성 - * @param {JSX.Element} props.fallback - 서버 렌더링 시 표시할 대체 요소 * @param {React.ReactNode} props.children - 클라이언트에서 렌더링할 자식 요소 - * @returns {React.ReactNode} - 서버에서는 fallback을, 클라이언트에서는 children을 반환 + * @param {React.ReactNode} props.fallback - 서버 렌더링 시 표시할 대체 요소 + * @returns {JSX.Element} - 서버에서는 fallback을, 클라이언트에서는 children을 반환 + * + * @example + * 서버 환경입니다.}> + *
클라이언트 환경입니다.
+ *
*/ export function ClientGate({ - fallback = <>, + fallback, children, -}: PropsWithChildren): ReactNode { +}: ClientGateProps): JSX.Element { const isServer = useSyncExternalStore( subscribe, getSnapshot, getServerSnapshot ); - return isServer ? fallback : children; + return <>{isServer ? fallback : children}; } diff --git a/packages/react/src/components/IfElse/IfElse.spec.tsx b/packages/react/src/components/IfElse/IfElse.spec.tsx index 6efae36fe..db766192c 100644 --- a/packages/react/src/components/IfElse/IfElse.spec.tsx +++ b/packages/react/src/components/IfElse/IfElse.spec.tsx @@ -3,22 +3,22 @@ import { render, screen } from '@testing-library/react'; import { IfElse } from '.'; describe('IfElse', () => { - const TrueComponent = () => { + const TruthyComponent = () => { return

true

; }; - const FalseComponent = () => { + const FalsyComponent = () => { return

false

; }; - describe('When condition prop type is boolean', () => { - it('should render the trueComponent when the condition prop is true', () => { + describe('condition prop이 boolean 타입일 때', () => { + it('condition prop이 true일 때 trueComponent를 렌더링해야 한다', () => { const condition = true; render( } - falseComponent={} + truthyComponent={} + falsyComponent={} /> ); const trueHeader = screen.queryByText('true'); @@ -27,13 +27,13 @@ describe('IfElse', () => { expect(falseHeader).not.toBeInTheDocument(); }); - it('should render the falseComponent when the condition prop is false', () => { + it('condition prop이 false일 때 falseComponent를 렌더링해야 한다', () => { const condition = false; render( } - falseComponent={} + truthyComponent={} + falsyComponent={} /> ); const trueHeader = screen.queryByText('true'); @@ -43,14 +43,14 @@ describe('IfElse', () => { }); }); - describe('When condition prop type is function', () => { - it('should render the trueComponent when the condition prop function returns true', () => { + describe('condition prop이 함수 타입일 때', () => { + it('condition prop 함수가 true를 반환할 때 trueComponent를 렌더링해야 한다', () => { const condition = () => true; render( } - falseComponent={} + truthyComponent={} + falsyComponent={} /> ); @@ -60,13 +60,13 @@ describe('IfElse', () => { expect(falseHeader).not.toBeInTheDocument(); }); - it('should render the falseComponent when the condition prop function returns false', () => { + it('condition prop 함수가 false를 반환할 때 falseComponent를 렌더링해야 한다', () => { const condition = () => false; render( } - falseComponent={} + truthyComponent={} + falsyComponent={} /> ); const trueHeader = screen.queryByText('true'); diff --git a/packages/react/src/components/IfElse/index.tsx b/packages/react/src/components/IfElse/index.tsx index 19b690531..6a7792953 100644 --- a/packages/react/src/components/IfElse/index.tsx +++ b/packages/react/src/components/IfElse/index.tsx @@ -4,23 +4,35 @@ type Condition = boolean | (() => boolean); interface IfElseProps { condition: Condition; - trueComponent: React.ReactNode; - falseComponent: React.ReactNode; + truthyComponent: React.ReactNode; + falsyComponent: React.ReactNode; } const getConditionResult = (condition: Condition) => { return typeof condition === 'function' ? condition() : condition; }; +/** + * @description If-Else 조건부 렌더링을 사용하기 위한 컴포넌트입니다. + * + * @param {IfElseProps} props + * @param {Condition} props.condition - 렌더링 조건 (boolean 또는 boolean을 반환하는 함수) + * @param {React.ReactNode} props.truthyComponent - condition이 true일 때 렌더링될 컴포넌트 + * @param {React.ReactNode} props.falsyComponent - condition이 false일 때 렌더링될 컴포넌트 + * @returns {JSX.Element} 조건에 따라 trueComponent 또는 falseComponent를 렌더링 + * + * @example + * } + * falsyComponent={} + * /> + */ export const IfElse = ({ condition, - trueComponent, - falseComponent, + truthyComponent, + falsyComponent, }: IfElseProps): JSX.Element => { const conditionResult = getConditionResult(condition); - return ( - - {conditionResult ? trueComponent : falseComponent} - - ); + return <>{conditionResult ? truthyComponent : falsyComponent}; }; diff --git a/packages/react/src/components/Mounted/index.tsx b/packages/react/src/components/Mounted/index.tsx index c4d692489..63bb06f46 100644 --- a/packages/react/src/components/Mounted/index.tsx +++ b/packages/react/src/components/Mounted/index.tsx @@ -1,15 +1,26 @@ import { useIsMounted } from '../../hooks/useIsMounted'; interface MountedProps { - fallback?: JSX.Element; + children: React.ReactNode; + fallback?: React.ReactNode; } -export const Mounted = ({ - fallback = <>, - children, -}: React.PropsWithChildren) => { +/** + * @description 컴포넌트가 마운트된 후에만 children을 렌더링하는 컴포넌트입니다. + * + * @param {MountedProps} props + * @param {React.ReactNode} props.children - 마운트된 후 렌더링될 자식 컴포넌트 + * @param {React.ReactNode} props.fallback - 마운트되기 전에 표시될 대체 컴포넌트 (선택사항) + * @returns {JSX.Element} 마운트 상태에 따라 children 또는 fallback을 렌더링 + * + * @example + * fallback component}> + *
children component
+ *
+ */ +export const Mounted = ({ fallback, children }: MountedProps): JSX.Element => { const isMounted = useIsMounted(); - if (!isMounted) return fallback; + if (!isMounted) return <>{fallback}; return <>{children}; }; diff --git a/packages/react/src/components/When/When.spec.tsx b/packages/react/src/components/When/When.spec.tsx index dc08829f9..6f290a67d 100644 --- a/packages/react/src/components/When/When.spec.tsx +++ b/packages/react/src/components/When/When.spec.tsx @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import { When } from '.'; -describe('When Component', () => { - it('should render the child element when the condition prop is true', () => { +describe('When', () => { + it('condition prop이 true일 때 자식 요소를 렌더링해야 한다', () => { render(

render

@@ -15,7 +15,7 @@ describe('When Component', () => { expect(header).toBeInTheDocument(); }); - it('should not render the child element when the condition prop is false', () => { + it('condition prop이 false일 때 자식 요소를 렌더링하지 않아야 한다', () => { render(

render

@@ -27,7 +27,7 @@ describe('When Component', () => { expect(header).not.toBeInTheDocument(); }); - it('should render the child element when the condition prop function returns true', () => { + it('condition prop 함수가 true를 반환할 때 자식 요소를 렌더링해야 한다', () => { render( true}>

render

@@ -39,7 +39,7 @@ describe('When Component', () => { expect(header).toBeInTheDocument(); }); - it('should not render the child element when the condition prop function returns false', () => { + it('condition prop 함수가 false를 반환할 때 자식 요소를 렌더링하지 않아야 한다', () => { render( false}>

render

@@ -51,7 +51,7 @@ describe('When Component', () => { expect(header).not.toBeInTheDocument(); }); - it('should render fallback when condition is false and has fallback props', () => { + it('condition이 false이고 fallback prop이 있을 때 fallback을 렌더링해야 한다', () => { render( false

}> diff --git a/packages/react/src/components/When/index.tsx b/packages/react/src/components/When/index.tsx index 1dec78c9a..d7c398717 100644 --- a/packages/react/src/components/When/index.tsx +++ b/packages/react/src/components/When/index.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { PropsWithChildren } from 'react'; type Condition = boolean | (() => boolean); interface WhenProps { + children: React.ReactNode; condition: Condition; fallback?: React.ReactNode; } @@ -12,13 +12,33 @@ const getConditionResult = (condition: Condition) => { return typeof condition === 'function' ? condition() : condition; }; +/** + * @description condition prop이 true일 때 children을 렌더링하고, false일 때는 fallback을 렌더링하는 조건부 렌더링 컴포넌트입니다. + * + * @param {WhenProps} props + * @param {React.ReactNode} props.children - 조건이 참일 때 렌더링될 자식 요소 + * @param {Condition} props.condition - 렌더링 여부를 결정하는 조건. boolean 값이나 boolean을 반환하는 함수 + * @param {React.ReactNode} props.fallback - 조건이 거짓일 때 렌더링될 대체 요소 (선택사항) + * @returns {JSX.Element} 조건에 따라 children 또는 fallback을 렌더링 + * + * @example + * ```tsx + * + * + * + * + * fallback component}> + * + * + * ``` + */ export const When = ({ children, condition, - fallback = null, -}: PropsWithChildren) => { + fallback, +}: WhenProps): JSX.Element => { const conditionResult = getConditionResult(condition); - if (!conditionResult) return {fallback}; - return {children}; + if (!conditionResult) return <>{fallback}; + return <>{children}; };