-
Notifications
You must be signed in to change notification settings - Fork 20
feat(react): useInterval start/stop interval 동작 제어 함수 추가 #178
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
Conversation
🦋 Changeset detectedLatest commit: 2b87cd9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
const delay = isNumber(options) ? options : options.delay; | ||
const enabled = isNumber(options) ? true : options?.enabled ?? true; | ||
|
||
const preservedOptions = usePreservedState(options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
options
는 객체
로 받아 올 수 있기 때문에 내부 데이터의 변화가 없다면 usePreservedState
로 참조를 유지합니다.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #178 +/- ##
==========================================
+ Coverage 95.14% 95.21% +0.06%
==========================================
Files 78 78
Lines 762 773 +11
Branches 185 187 +2
==========================================
+ Hits 725 736 +11
Misses 31 31
Partials 6 6
|
0a315e5
to
3938ab3
Compare
useEffect(() => { | ||
if (!isNumberOptions) { | ||
setEnabled(preservedOptions?.enabled ?? true); | ||
} | ||
}, [isNumberOptions, preservedOptions]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useState의 기본 값(initialState)
을 설정한다면 해당 컴포넌트 혹은 훅은 리렌더링 되더라도 기본 값이 유지되는 특징이 있기 때문에 useEffect
로 변화를 감지해서 데이터를 갱신합니다.
https://ko.react.dev/reference/react/useState#parameters
initialState: state의 초기 설정값입니다. 어떤 유형의 값이든 지정할 수 있지만 함수에 대해서는 특별한 동작이 있습니다.
이 인수는 초기 렌더링 이후에는 무시됩니다.
https://ko.react.dev/reference/react/useState#avoiding-recreating-the-initial-state
React는 초기 state를 한 번 저장하고 다음 렌더링부터는 이를 무시합니다.
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
createInitialTodos()의 결과는 초기 렌더링에만 사용되지만, 여전히 모든 렌더링에서 이 함수를 호출합니다. 이는 큰 배열을 생성하거나 값비싼 계산을 수행하는 경우 낭비일 수 있습니다.
이 문제를 해결하려면, useState에 초기화 함수로 전달하세요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ssi02014 상세한 설명 감사합니다! 😄
위와 같이 구현하신 이유는 결국
- initialValue를 그대로 state로 사용하면 re-render가 일어나더라도 해당 값을 계속 유지하기때문에 이 문제를 해결하기 위해 함수형으로 전달하여 최초 렌더링에서만 값이 적용될 수 있도록 함. (enabled state)
- 이후 props로 객체를 받아오기때문에 정확한 비교(deepEqual)를 적용하여 최적화한 usePreservedState를 사용하고, 이 값이 바뀌었을때만 다시 enabled state를 set 하여 변경이 정확하게 적용될 수 있도록 함.
이렇게 이해하면 될까요 ?? 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제 의도를 다시 정리해서 말씀드려보겠습니다!
- initialValue를 그대로 state로 사용하면 re-render가 일어나더라도 해당 값을 계속 유지하기때문에 이 문제를 해결하기 위해 함수형으로 전달하여 최초 렌더링에서만 값이 적용될 수 있도록 함. (enabled state)
정확히는 초기화 함수
, 값
모두 InitialValue
로 활용하면 리렌더링 시에 해당 초기 렌더링 시점의 결과값이 유지
됩니다. 단 약간의 차이가 있는데요 그냥 값 자체를 initialValue로 활용 할 경우 리렌더링이 발생할 때마다 결과값은 그대로 사용되지만, 계산은 매번 이뤄집니다.
예를 들어, useState(Array(10000).fill(0).map())
과 같은 로직이 있다면 리렌더링 시 매번 길이 10000의 배열을 생성하고 이를 처리하는 로직이 실행됩니다. (단, 초기 렌더링 시에 계산된 배열이 계속 활용 됨) 따라서, 이런 초기화 로직이 만약 리소스가 크다면 매번 계산되는 것은 비효율적일 수 있습니다.
하지만 초기화 함수
로 넘길 경우에는 초기 렌더링의 결과값을 그대로 사용하는 것은 동일하지만, 리렌더링 시 계산하는 것 조차 재시도하지 않습니다. 따라서 함수로 활용하는 것이 리소스면에서 효율적일 수 있습니다.
useState(() => Array(10000).fill(0).map())
함수로 넘길 경우 리렌더링 시 매번 길이 10000의 배열을 생성하고, 이를 처리하는 로직이 실행되지 않습니다.
이게 react.dev에 나온 설명에 대한 관련 내용입니다.
createInitialTodos()의 결과는
초기 렌더링에만 사용되지만, 여전히 모든 렌더링에서 이 함수를 호출
합니다. 이는 큰 배열을 생성하거나 값비싼 계산을 수행하는 경우 낭비일 수 있습니다.
이 문제를 해결하려면, useState에 초기화 함수로 전달
하세요.
⭐️ 여기까지 정리하면! useState의 초기값을 설정하면, options props가 변경되어 useInterval이 리렌더링 되더라도 최초 options의 값을 기준으로 enabled state가 그대로 유지하기 때문에 useEffect에서 이를 감지하고 갱신하도록 하였습니다.
- 이렇게 options가 변경되는 케이스는 다음과 같습니다.
{ delay: 500 enabled: toggle }
와 같이 toggle state를 enabled로 활용될 수 있습니다. - 외부에서 toggle을 통해 options의 enabled를 변경하더라도 최초 toggle 값으로 enabled가 유지됩니다. (useState의 초기값 유지 특징 때문에)
- 이후 props로 객체를 받아오기때문에 정확한 비교(deepEqual)를 적용하여 최적화한 usePreservedState를 사용하고, 이 값이 바뀌었을때만 다시 enabled state를 set 하여 변경이 정확하게 적용될 수 있도록 함.
usePreservedState
를 사용하게 된 이유를 말씀드리겠습니다! 예를 들어, options props가 객체인데 usePrservedState
로 매핑하는게 아닌 아래와 같이 options props 그대로 사용해 useEffect를 구성했다고 가정하면
useEffect(() => {
if (!isNumberOptions) {
setEnabled(options?.enabled ?? true);
}
}, [isNumberOptions, options]);
stopInterval과 같은 enabled State를 바꿀 때 options 역시 새로운 객체로 판단해 useEffect가 트리거됩니다.
따라서, 기존 인자로 받은 options의 enable의 값이 true였다면, false로 변경됐다가 다시 해당 useEffect가 트리거되서 true로 변경됩니다.
options: { delay: 500, enabled: true } 로 가정
내부 enabled State 변화
1. 초기 값 true
2. stopInterval 클릭
3. false로 변화
4. options가 객체이기 떄문에 useEffect 트리거 (이때, props로 받은 options의 enabled는 true임)
5. true로 변화
options 내부 값 자체가 변경되지 않으면 같은 객체로 판단 할 수 있게(즉, 4번에서 useEffect에 트리거되지 않게) preservedState로 참조를 유지하도록 하였습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ssi02014 상세히 설명해주셔서 감사합니다! 👍👍👍
1번 내용의 경우 확인차 여쭤보려고 한 부분이 말씀해주신 부분과 맥락은 같았는데 말씀드릴때 도약이 있어서 의도하신 바를 다르게 표현된듯하네요 😂
꼼꼼하게 확인해주시고 설명해주셔서 다시한번 너무 감사합니다! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sangminnn 앗 맞습니다! 제 의도를 다시한번 정리해서 다시 말씀드린 부분이라고 이해해주시면 감사드립니다! 🙇🏻♂️
@@ -30,4 +50,6 @@ export const useInterval = ( | |||
|
|||
return () => clearInterval(intervalId); | |||
}, [callbackAction, enabled, delay]); | |||
|
|||
return { isActing: enabled, startInterval, stopInterval }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사소하지만 훅 내부에서 관리하는 enabled는 결국 interval의 동작 여부
를 의미합니다. 누군가는 interval 동작 여부를 외부에서 다루는 것을 원할 수 있습니다. 따라서 반환 데이터로 이를 추가했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전반적으로 확인해보았는데 너무 좋은것같습니다! 고생 많으셨습니다! 👍
(항상 많이 배워갑니다! 🙇 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
너무 좋습니다!!
한 가지 의견 드려보고 싶은 게 있어서 의견 남겨봅니다!!
단순 제안이라, Approve 입니다!
const { isActing, stopInterval, startInterval } = useInterval( | ||
() => setNumber(number + 1), | ||
{ delay: 1000, enabled: false } | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 이 useInterval의 return value 를 쓰시는 분들 중에도 비구조화할당을 안 하고 쓰시는 분이 많을 것 같고 예상되는 인터페이스는 다음과 같습니다.
const interval = useInterval(callback, 500)
interval.startInerval();
interval.stopInterval();
그래서 제가 제안 드리는 것은 stop, start 으로 네이밍하면 어떨까요? useQuery에서 refetchQuery 로 지은 게 아닌 것처럼요!
제안 드린 배경
제가 useQuery 할 때 변경된 하나의 습관?! 이라서 의견 남겨보고 싶습니다 제가 원래 사용하던 useQuery의 인터페이스는 다음과 같습니다.const { data, isLoading } = useQuery(fooQueryOption);
하나만 호출되어야 하는 경우에는 편하게 사용할 수 있습니다. 하지만 다음과 같이
const { data: fooData, isLoading: fooIsLoading } = useQuery(fooQueryOption);
const { data: barData, isLoading: barIsLoading } = useQuery(barQueryOption);
불필요하게 비구조화할당을 이상하게 쓰고 있더라구요
그래서 비구조화 할당을 안 하는 습관을 들이게 되었습니다.
const foo = useQuery(fooQueryOption);
const bar = useQuery(barQueryOption);
// ...
foo.data // 와 같이 접근
bar.data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@minsoo-web 처음에는 접미사로 interval을 추가하는게 조금 더 명확하지 않을까!? 생각했는데 좋습니다!
useInterval의 반환 값이다보니 interval 접미사에 대한 부분이 사실 꼭 필요하지는 않을 것 같네요.(유추가능)
만약 네이밍 변경이 필요하다면 예제를 주신 것처럼 사용자가 구조분해를 통해 직접 네이밍을 변경하는 방향으로 하면 좋겠네요
3938ab3
to
415e619
Compare
Co-authored-by: Minsoo Kim <[email protected]> Co-authored-by: Sangminnn <[email protected]>
415e619
to
2b87cd9
Compare
@Sangminnn @minsoo-web 지금까지 많은 아이디어를 주셨는데 공동 작업자(co-author)추가를 놓치고 있었네요! 공동 작업자로 추가하였습니다 😄 |
Co-author 까지,, 너무 영광입니다 감사해요! |
@minsoo-web 좋은 아이디어 주셔서 제가 항상 더 감사합니다 🙇🏻♂️ |
저도 감사합니다! 두분 이야기 나누시는것도 보면서 많이 배우고있습니다! 🙇 |
@Sangminnn @minsoo-web toss/slash#476 (comment) |
Overview
Issue: #173 (review)
cc. @minsoo-web
useInterval에서 interval의 동작을 선언적으로 처리할 수 있는
startInterval
,stopInterval
함수를 추가합니다.누군가는 컴포넌트 마운트 후에
interval의 자동 실행
을 원하지 않을 수 있습니다. 따라서 기존enabled
옵션은 남겨놓았습니다.물론, enabled 옵션을 제공안하더라도 직접 useEffect를 통해서 Interval 실행을 멈출 수 있지만, useEffect를 추가로 작성해줘야 하기 때문에 코드의 복잡도가 늘어 날 수 있습니다.
따라서, 아래와 같이 useEffect를 추가하는 방향보다 enabled 옵션을 열어놓는 방향으로 진행하였습니다.
PR Checklist
Contributing Guide