diff --git a/package.json b/package.json index c67afa9..6ef0bee 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "ci:version": "changeset version", "logging-system": "pnpm --filter @yourssu/logging-system-react", "utils": "pnpm --filter @yourssu/utils", + "react": "pnpm --filter @yourssu/react", "prepare": "husky", "test": "vitest", "test:ui": "vitest --ui" diff --git a/packages/react/README.md b/packages/react/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000..45fa44d --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,39 @@ +{ + "name": "@yourssu/react", + "private": false, + "version": "0.1.0", + "description": "Yourssu React Package", + "keywords": [ + "yourssu", + "react" + ], + "repository": { + "type": "git", + "url": "https://github.com/yourssu/Yrano", + "directory": "packages/react" + }, + "license": "MIT", + "type": "module", + "types": "./dist/index.d.ts", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "scripts": { + "build": "tsc && tsup", + "test": "vitest" + }, + "devDependencies": { + "@testing-library/react": "^15.0.7" + } +} diff --git a/packages/react/src/hooks/useInterval.ko.md b/packages/react/src/hooks/useInterval.ko.md new file mode 100644 index 0000000..ce6e27e --- /dev/null +++ b/packages/react/src/hooks/useInterval.ko.md @@ -0,0 +1,27 @@ +# useInterval + +`useInterval` 커스텀 훅은 특정 간격마다 주어진 콜백 함수를 실행하기 위해 사용됩니다. 이 훅은 `useEffect`와 `useRef`를 활용하여 콜백 함수와 간격 타이머를 관리합니다. + +## 매개변수 + +- `callback`: 실행될 콜백 함수입니다. +- `interval`: 콜백 함수가 실행될 간격(밀리초)입니다. + +## Example + +```jsx +const MyComponent = () => { + useInterval(() => { + console.log('This will run every second'); + }, 1000); + + return
Check the console
; +}; +``` + +## Notes +이 훅은 내부적으로 두 개의 `useEffect`를 사용합니다: +1. 첫 번째 `useEffect`는 콜백 함수를 최신 상태로 유지합니다. +2. 두 번째 `useEffect`는 주어진 간격마다 콜백 함수를 실행하는 타이머를 설정합니다. + +`interval`이 변경되거나 `null`로 설정되면 타이머가 갱신되거나 해제됩니다. diff --git a/packages/react/src/hooks/useInterval.test.ts b/packages/react/src/hooks/useInterval.test.ts new file mode 100644 index 0000000..65f6bf1 --- /dev/null +++ b/packages/react/src/hooks/useInterval.test.ts @@ -0,0 +1,79 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; + +import { useInterval } from './useInterval'; // Adjust the import path to your actual useInterval hook + +describe('useInterval', () => { + it('should call the callback function at the given interval', () => { + vi.useFakeTimers(); + + const callback = vi.fn(); + const interval = 1000; + + renderHook(() => useInterval(callback, interval)); + + expect(callback).not.toHaveBeenCalled(); + + act(() => { + vi.advanceTimersByTime(interval); + }); + + expect(callback).toHaveBeenCalledTimes(1); + + act(() => { + vi.advanceTimersByTime(interval); + }); + + expect(callback).toHaveBeenCalledTimes(2); + + vi.useRealTimers(); + }); + + it('should not call the callback function when the interval is null', () => { + vi.useFakeTimers(); + + const callback = vi.fn(); + const interval = null; + + renderHook(() => useInterval(callback, interval as unknown as number)); + + expect(callback).not.toHaveBeenCalled(); + + act(() => { + vi.advanceTimersByTime(1000); + }); + + expect(callback).not.toHaveBeenCalled(); + + vi.useRealTimers(); + }); + + it('should update the interval when the interval changes', () => { + vi.useFakeTimers(); + + const callback = vi.fn(); + let interval = 1000; + + const { rerender } = renderHook(() => useInterval(callback, interval)); + + expect(callback).not.toHaveBeenCalled(); + + act(() => { + vi.advanceTimersByTime(interval); + }); + + expect(callback).toHaveBeenCalledTimes(1); + + interval = 2000; + + rerender(); + + act(() => { + vi.advanceTimersByTime(interval); + }); + + expect(callback).toHaveBeenCalledTimes(2); + + vi.useRealTimers(); + }); +}); diff --git a/packages/react/src/hooks/useInterval.ts b/packages/react/src/hooks/useInterval.ts new file mode 100644 index 0000000..a4d52c1 --- /dev/null +++ b/packages/react/src/hooks/useInterval.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef } from 'react'; + +interface UseIntervalProps { + (callback: () => void, interval: number): void; +} + +/** + * `useInterval` 커스텀 훅은 특정 간격마다 주어진 콜백 함수를 실행합니다. + * + * @param {() => void} callback - 실행될 콜백 함수입니다. + * @param {number} interval - 콜백 함수가 실행될 간격(밀리초)입니다. + * + * @remarks + * 이 훅은 내부적으로 두 개의 `useEffect`를 사용합니다: + * 1. 첫 번째 `useEffect`는 콜백 함수를 최신 상태로 유지합니다. + * 2. 두 번째 `useEffect`는 주어진 간격마다 콜백 함수를 실행하는 타이머를 설정합니다. + * + * `interval`이 변경되거나 `null`로 설정되면 타이머가 갱신되거나 해제됩니다. + */ +export const useInterval: UseIntervalProps = (callback: () => void, interval: number) => { + const savedCallback = useRef<(() => void) | null>(null); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + function tick() { + if (savedCallback.current) { + savedCallback.current(); + } + } + if (interval !== null) { + const id = setInterval(tick, interval); + return () => clearInterval(id); + } + }, [interval]); +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000..77f12e5 --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1 @@ +export { useInterval } from './hooks/useInterval.ts'; diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000..6435d43 --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts new file mode 100644 index 0000000..4163141 --- /dev/null +++ b/packages/react/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['./src/index.ts'], + format: ['cjs', 'esm'], + dts: { + entry: './src/index.ts', + resolve: true, + }, + external: ['react', 'react-dom'], + splitting: false, + clean: true, + sourcemap: true, + minify: true, + treeshake: true, + skipNodeModulesBundle: true, + outDir: './dist', +}); diff --git a/packages/react/vitest.config.ts b/packages/react/vitest.config.ts new file mode 100644 index 0000000..5f24654 --- /dev/null +++ b/packages/react/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + dir: './src', + globals: true, + environment: 'jsdom', + }, +}); diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index d5a720d..6435d43 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -2,11 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "./src", - "outDir": "./dist", - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } + "outDir": "./dist" }, "include": ["src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89f0967..576984d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,12 @@ importers: specifier: ^6.21.3 version: 6.23.1(react-dom@18.3.1)(react@18.3.1) + packages/react: + devDependencies: + '@testing-library/react': + specifier: ^15.0.7 + version: 15.0.7(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + packages/utils: {} packages: @@ -1566,10 +1572,47 @@ packages: lodash: 4.17.21 dev: true + /@testing-library/dom@10.1.0: + resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.24.6 + '@babel/runtime': 7.24.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/react@15.0.7(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@testing-library/dom': 10.1.0 + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: true + /@types/argparse@1.0.38: resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} dev: true + /@types/aria-query@5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -2098,6 +2141,12 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + /array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -2672,6 +2721,11 @@ packages: engines: {node: '>=0.4.0'} dev: true + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + /detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2703,6 +2757,10 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -4319,6 +4377,11 @@ packages: yallist: 4.0.0 dev: true + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} dependencies: @@ -4864,6 +4927,15 @@ packages: hasBin: true dev: true + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4925,6 +4997,10 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + /react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} dev: true diff --git a/tsconfig.json b/tsconfig.json index dbac048..30382ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,10 +13,6 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - }, /* Linting */ "strict": true,