Skip to content

Commit

Permalink
fix(react): ClientGate, IfElse, When, Mounted 인터페이스 개선 (#710)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssi02014 authored Jan 25, 2025
1 parent 8f28e1b commit 2ab42b6
Showing 10 changed files with 145 additions and 72 deletions.
5 changes: 5 additions & 0 deletions .changeset/hot-rabbits-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modern-kit/react': patch
---

fix(react): ClientGate, IfElse, When, Mounted 인터페이스 개선 - @ssi02014
20 changes: 14 additions & 6 deletions docs/docs/react/components/ClientGate.mdx
Original file line number Diff line number Diff line change
@@ -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

<br />

@@ -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<ClientGateProps>) => JSX.Element;
function ClientGate({ fallback, children }: ClientGateProps): JSX.Element;
```

## Usage
24 changes: 14 additions & 10 deletions docs/docs/react/components/IfElse.mdx
Original file line number Diff line number Diff line change
@@ -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 = () => {
<button onClick={() => setState(!state)}>Toggle Button</button>
<IfElse
condition={state}
trueComponent={<h1>true component</h1>}
falseComponent={<h1>false component</h1>}
truthyComponent={<h1>true component</h1>}
falsyComponent={<h1>false component</h1>}
/>
</>
);
@@ -55,8 +59,8 @@ export const Example = () => {
<button onClick={() => setState(!state)}>Toggle Button</button>
<IfElse
condition={state}
trueComponent={<h1>true component</h1>}
falseComponent={<h1>false component</h1>}
truthyComponent={<h1>true component</h1>}
falsyComponent={<h1>false component</h1>}
/>
</>
);
7 changes: 2 additions & 5 deletions docs/docs/react/components/When.mdx
Original file line number Diff line number Diff line change
@@ -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<WhenProps>) => JSX.Element;
const When: ({ children, condition, fallback }: WhenProps) => JSX.Element;
```

## Usage
34 changes: 25 additions & 9 deletions packages/react/src/components/ClientGate/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,48 @@
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;
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
* <ClientGate fallback={<div>서버 환경입니다.</div>}>
* <div>클라이언트 환경입니다.</div>
* </ClientGate>
*/
export function ClientGate({
fallback = <></>,
fallback,
children,
}: PropsWithChildren<ClientGateProps>): ReactNode {
}: ClientGateProps): JSX.Element {
const isServer = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot
);

return isServer ? fallback : children;
return <>{isServer ? fallback : children}</>;
}
32 changes: 16 additions & 16 deletions packages/react/src/components/IfElse/IfElse.spec.tsx
Original file line number Diff line number Diff line change
@@ -3,22 +3,22 @@ import { render, screen } from '@testing-library/react';
import { IfElse } from '.';

describe('IfElse', () => {
const TrueComponent = () => {
const TruthyComponent = () => {
return <p role="document">true</p>;
};

const FalseComponent = () => {
const FalsyComponent = () => {
return <p role="paragraph">false</p>;
};

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(
<IfElse
condition={condition}
trueComponent={<TrueComponent />}
falseComponent={<FalseComponent />}
truthyComponent={<TruthyComponent />}
falsyComponent={<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(
<IfElse
condition={condition}
trueComponent={<TrueComponent />}
falseComponent={<FalseComponent />}
truthyComponent={<TruthyComponent />}
falsyComponent={<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(
<IfElse
condition={condition}
trueComponent={<TrueComponent />}
falseComponent={<FalseComponent />}
truthyComponent={<TruthyComponent />}
falsyComponent={<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(
<IfElse
condition={condition}
trueComponent={<TrueComponent />}
falseComponent={<FalseComponent />}
truthyComponent={<TruthyComponent />}
falsyComponent={<FalsyComponent />}
/>
);
const trueHeader = screen.queryByText('true');
30 changes: 21 additions & 9 deletions packages/react/src/components/IfElse/index.tsx
Original file line number Diff line number Diff line change
@@ -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
* <IfElse
* condition={condition}
* truthyComponent={<TruthyComponent />}
* falsyComponent={<FalsyComponent />}
* />
*/
export const IfElse = ({
condition,
trueComponent,
falseComponent,
truthyComponent,
falsyComponent,
}: IfElseProps): JSX.Element => {
const conditionResult = getConditionResult(condition);
return (
<React.Fragment>
{conditionResult ? trueComponent : falseComponent}
</React.Fragment>
);
return <>{conditionResult ? truthyComponent : falsyComponent}</>;
};
23 changes: 17 additions & 6 deletions packages/react/src/components/Mounted/index.tsx
Original file line number Diff line number Diff line change
@@ -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<MountedProps>) => {
/**
* @description 컴포넌트가 마운트된 후에만 children을 렌더링하는 컴포넌트입니다.
*
* @param {MountedProps} props
* @param {React.ReactNode} props.children - 마운트된 후 렌더링될 자식 컴포넌트
* @param {React.ReactNode} props.fallback - 마운트되기 전에 표시될 대체 컴포넌트 (선택사항)
* @returns {JSX.Element} 마운트 상태에 따라 children 또는 fallback을 렌더링
*
* @example
* <Mounted fallback={<div>fallback component</div>}>
* <div>children component</div>
* </Mounted>
*/
export const Mounted = ({ fallback, children }: MountedProps): JSX.Element => {
const isMounted = useIsMounted();

if (!isMounted) return fallback;
if (!isMounted) return <>{fallback}</>;
return <>{children}</>;
};
12 changes: 6 additions & 6 deletions packages/react/src/components/When/When.spec.tsx
Original file line number Diff line number Diff line change
@@ -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(
<When condition={true}>
<p role="paragraph">render</p>
@@ -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(
<When condition={false}>
<p role="paragraph">render</p>
@@ -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(
<When condition={() => true}>
<p role="paragraph">render</p>
@@ -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(
<When condition={() => false}>
<p role="paragraph">render</p>
@@ -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(
<When condition={false} fallback={<p role="paragraph">false</p>}>
<button>true</button>
Loading

0 comments on commit 2ab42b6

Please sign in to comment.