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}>;
};