Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Life cycle hook 개발 #3

Merged
merged 12 commits into from
Oct 31, 2022
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = {
'import/no-unresolved': 'error',
'react/react-in-jsx-scope': 'off',
'no-use-before-define': 'off',
'no-restricted-exports': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'simple-import-sort/imports': [
'error',
Expand All @@ -94,7 +95,10 @@ module.exports = {
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ ignoreRestSiblings: true, argsIgnorePattern: '_', varsIgnorePattern: '_' },
],
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'error',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"start": "next start",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
"test:unit": "jest",
"test:watch": "jest --watch",
"test:coverage": "yarn test:unit --coverage",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
Expand Down
7 changes: 7 additions & 0 deletions src/hooks/useDidMount/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Default from './index';

describe('useDidMount index', () => {
it('default export이여야 한다', () => {
expect(Default).toBeDefined();
});
});
1 change: 1 addition & 0 deletions src/hooks/useDidMount/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useDidMount';
59 changes: 59 additions & 0 deletions src/hooks/useDidMount/useDidMount.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';

import useDidMount from './useDidMount';

describe('useDidMount', () => {
it('default export이여야 한다', () => {
expect(useDidMount).toBeDefined();
});

it('effectCallback이 실행되어야 한다', () => {
const effectCallback = jest.fn();
renderHook(() => useDidMount(effectCallback));
expect(effectCallback).toBeCalled();
});

it('rerender 시 1번 실행되어야 한다', () => {
const effectCallback = jest.fn();
const { rerender } = renderHook(() => useDidMount(effectCallback));
rerender();
expect(effectCallback).toBeCalledTimes(1);
});

describe('useDidMount Component', () => {
const STATE_CHANGE_BUTTON_TEXT = 'change';
const mockCallback = jest.fn();

const App = () => {
const [_, setState] = useState(false);

useDidMount(mockCallback);
return (
<div>
<button type="button" onClick={() => setState((prev) => !prev)}>
{STATE_CHANGE_BUTTON_TEXT}
</button>
</div>
);
};

afterEach(() => {
jest.clearAllMocks();
});

it('mockCallback이 실행되어야 한다', () => {
render(<App />);
expect(mockCallback).toBeCalledTimes(1);
});

it('mockCallback은 상태가 변해도 1번 실행되어야 한다', () => {
render(<App />);
expect(mockCallback).toBeCalledTimes(1);
const setStateButton = screen.getByText(STATE_CHANGE_BUTTON_TEXT);
fireEvent.click(setStateButton);
expect(mockCallback).toBeCalledTimes(1);
});
});
});
13 changes: 13 additions & 0 deletions src/hooks/useDidMount/useDidMount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EffectCallback, useEffect, useRef } from 'react';

const useDidMount = (effectCallback: EffectCallback) => {
const didMountRef = useRef<boolean>(false);

useEffect(() => {
if (didMountRef.current) return;
didMountRef.current = true;
effectCallback();
}, []);
};

export default useDidMount;
7 changes: 7 additions & 0 deletions src/hooks/useDidUpdate/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Default from './index';

describe('useDidUpdate index', () => {
it('default export이어야 한다', () => {
expect(Default).toBeDefined();
});
});
1 change: 1 addition & 0 deletions src/hooks/useDidUpdate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useDidUpdate';
54 changes: 54 additions & 0 deletions src/hooks/useDidUpdate/useDidUpdate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useState } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';

import useDidUpdate from './useDidUpdate';

describe('useDidUpdate', () => {
it('default export이여야 한다', () => {
expect(useDidUpdate).toBeDefined();
});

it('첫 호출시 effectCallback이 실행되면 안된다', () => {
const mockCallback = jest.fn();
renderHook(() => useDidUpdate(mockCallback, []));
expect(mockCallback).not.toBeCalled();
});

describe('useDidUpdate Component', () => {
const STATE_CHANGE_BUTTON_TEXT = 'change';
const mockCallback = jest.fn();

const App = () => {
const [state, setState] = useState<number>(0);

useDidUpdate(mockCallback, [state]);

return (
<div>
<button type="button" onClick={() => setState((prev) => prev + 1)}>
{STATE_CHANGE_BUTTON_TEXT}
</button>
</div>
);
};

afterEach(() => {
jest.clearAllMocks();
});

it('마운트 시 effectCallback이 실행되면 안된다', () => {
render(<App />);
expect(mockCallback).not.toBeCalled();
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭔가.. 간단하게 만들 수 있을 것 같은뎅..

import { renderHook } from '@testing-library/react-hooks';

import useDidMount from './useDidMount';

describe('useDidMount', () => {
  const effectCallback = jest.fn();

  beforeEach(() => {
    effectCallback.mockClear();
  });

  it('default export이여야 한다', () => {
    expect(useDidMount).toBeDefined();
  });

  it('effectCallback이 실행되어야 한다', () => {
    renderHook(() => useDidMount(effectCallback));
    expect(effectCallback).toBeCalled();
  });

  it('rerender 시 1번 실행되어야 한다', () => {
    const { rerender } = renderHook(() => useDidMount(effectCallback));
    rerender();
    expect(effectCallback).toBeCalledTimes(1);
  });

  it('mockCallback이 실행되어야 한다', () => {
    renderHook(() => useDidMount(effectCallback));

    expect(effectCallback).toBeCalledTimes(1);
  });

  it('mockCallback은 상태가 변해도 1번 실행되어야 한다', () => {
    const { rerender } = renderHook(() => useDidMount(effectCallback));

    rerender();

    expect(effectCallback).toBeCalledTimes(1);
  });
});

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 위에 똑같은 테스트코드가 있었네요 ㅋㅋ


it('dependency list 업데이트 시 effectCallback이 실행되어야 한다', () => {
render(<App />);
const setStateButton = screen.getByText(STATE_CHANGE_BUTTON_TEXT);
fireEvent.click(setStateButton);
expect(mockCallback).toBeCalledTimes(1);
fireEvent.click(setStateButton);
expect(mockCallback).toBeCalledTimes(2);
});
});
});
16 changes: 16 additions & 0 deletions src/hooks/useDidUpdate/useDidUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DependencyList, EffectCallback, useEffect, useRef } from 'react';

const useDidUpdate = (effectCallback: EffectCallback, dependencyList: DependencyList) => {
const didMountRef = useRef<boolean>(false);

useEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오오 이건 설마.. react18 대응인가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

18을 따로 대응할 생각은 없었어요 ㅋㅋㅋㅋ

아마 react18 dev에서 effect가 두 번 호출되는 사항을 말씀하신... 거죠..?

단순히 mount 시에는 실행되지 않게 하고, 디펜던시가 업데이트된 후에 실행되도록 작성하기 위한 부분이에요

논외지만 지금 생각해보니 did mount, did update에서 EffectCallback 타입이 아니라 VoidFunction 타입이 더 적절할 것 같네요 ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞아요. effect 두번 호출되는거.. ㅠ


effectCallback();
}, [...dependencyList]);
};

export default useDidUpdate;
7 changes: 7 additions & 0 deletions src/hooks/useWillUnmount/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Default from './index';

describe('useWillUnmount index', () => {
it('default export이어야 한다', () => {
expect(Default).toBeDefined();
});
});
1 change: 1 addition & 0 deletions src/hooks/useWillUnmount/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useWillUnmount';
63 changes: 63 additions & 0 deletions src/hooks/useWillUnmount/useWillUnmount.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState } from 'react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';

import useWillUnmount from './useWillUnmount';

describe('useWillUnmount', () => {
it('default export이어야 한다', () => {
expect(useWillUnmount).toBeDefined();
});

it('첫 호출시 callback이 실행되면 안된다', () => {
const mockCallback = jest.fn();
renderHook(() => useWillUnmount(mockCallback));
expect(mockCallback).not.toBeCalled();
});

describe('useWillUnmount Component', () => {
const TOGGLE_BUTTON_TEXT = 'toggle';
const mockCallback = jest.fn();

const Child = () => {
useWillUnmount(mockCallback);

return <div />;
};

const App = () => {
const [state, setState] = useState<boolean>(true);

return (
<div>
<button type="button" onClick={() => setState((prev) => !prev)}>
{TOGGLE_BUTTON_TEXT}
</button>

{state && <Child />}
</div>
);
};

afterEach(() => {
cleanup();
jest.clearAllMocks();
});

it('마운트 시 callback이 실행되면 안된다', () => {
render(<App />);
expect(mockCallback).not.toBeCalled();
});

it('Child가 unmount시 callback이 실행된다', () => {
render(<App />);
const toggleButton = screen.getByText(TOGGLE_BUTTON_TEXT);
fireEvent.click(toggleButton); // false
expect(mockCallback).toBeCalledTimes(1);

fireEvent.click(toggleButton); // true
fireEvent.click(toggleButton); // false
expect(mockCallback).toBeCalledTimes(2);
});
});
});
9 changes: 9 additions & 0 deletions src/hooks/useWillUnmount/useWillUnmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useEffect } from 'react';

const useWillUnmount = (callback: VoidFunction) => {
useEffect(() => {
return callback;
}, []);
};

export default useWillUnmount;