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,