Upgrade to Pro — share decks privately, control downloads, hide ads and more …

눈에 보이지 않는 개선: My구독의 Redux에서 React-Query 전환 경험 공유

kakao
PRO
December 08, 2022

눈에 보이지 않는 개선: My구독의 Redux에서 React-Query 전환 경험 공유

#React #React-Query #Tanstack-Query #Redux #리팩토링 #React16 #React18

My구독은 보이지 않은 개선을 진행하고 있는데요
그중에서 Redux에서 React-Query로 전환했던 그 이유와 과정을 소개하고 그 과정에서 만났던 이슈를 어떻게 해결했는지 공유합니다.
이번 발표를 통해서 React 16과 React 18에서 React-Query를 도입하거나 기존 Redux 구조를 개선하려고 시도하실 때 참고가 되었으면 좋겠습니다.

발표자 : fred.dy
카카오 FE플랫폼팀의 톡FE파트에서 프론트 앤드 개발자로 일하고 있는 프레드입니다. My구독과 이모티콘 스토어 프로젝트를 담당하고 있습니다.

kakao
PRO

December 08, 2022
Tweet

More Decks by kakao

Other Decks in Programming

Transcript

  1. My구독의 Redux에서 React
    -
    Query 전환 경험 공유


    Copyright 2022. Kakao Corp. All rights reserved. Redistribution or public display is not permitted without written permission from Kakao.
    눈에 보이지 않는 개선
    이동건 fred.dy


    카카오
    if(kakao)2022

    View Slide

  2. 보이지 않는 개선?

    View Slide

  3. Project
    부족한 일정
    새로운 기능
    새로운 기능
    기존 설계
    사용자 경험 개선
    새로운 기능
    완벽한 설계

    View Slide

  4. Project
    리팩터링
    새로운 기능
    새로운 기능
    기존 설계
    사용자 경험 개선
    새로운 기능

    View Slide

  5. My 구독

    View Slide

  6. 톡서랍 플러스 이모티콘 플러스
    My 구독
    카카오톡 데이터 백업


    팀채팅


    연락처


    패스워드



    다양한 이모티콘 사용


    이모티콘 자동 추천


    랜덤 이모티콘 추천


    검색



    View Slide

  7. 톡서랍 플러스
    이모티콘 플러스
    My 구독
    구독
    상태

    View Slide

  8. 구독 변경
    이용권
    인앱 결제

    View Slide

  9. Redux에서 React
    -
    Query 전환

    View Slide

  10. React Redux
    개발 경험이 있음 사용 경험이 있음

    View Slide

  11. React
    -
    Query 조금 알아보기


    Redux에서 React
    -
    Query로 전환 이유


    React
    -
    Query로 전환 과정


    정리

    View Slide

  12. React
    -
    Query 조금 알아보기


    Redux에서 React
    -
    Query로 전환 이유


    React
    -
    Query로 전환 과정


    정리

    View Slide

  13. 데이터 패칭 캐싱 동기화
    React
    -
    Query

    View Slide

  14. Why?
    “React
    -
    Query 없이 React 자체만으로는 불가능하나요?”

    View Slide

  15. React
    동기화
    캐싱
    const CacheContext = React.createContext(null);


    const { Provider } = CacheContext;


    function CacheProvider({ children }) {


    const [cache, setCache] = useState({});


    ...


    return {children};


    }
    데이터 패칭
    const [data, setData] = useState(null);


    const fetch = useCallback(() => { ... }, []);


    useEffect(() => { fetch(); }, []);


    return { data, refetch: fetch };
    useEffect(() => {


    const interval = setInterval(handleFocus, cacheTime)


    window.addEventListener('focus', handleFocus, false);


    ...


    return () => {


    clearInterval(interval);


    window.removeEventListener('focus', handleFocus, false);


    ...


    };


    }, []);

    View Slide

  16. React
    동기화
    캐싱
    const CacheContext = React.createContext(null);


    const { Provider } = CacheContext;


    function CacheProvider({ children }) {


    const [cache, setCache] = useState({});


    ...


    return {children};


    }
    데이터 패칭
    const [data, setData] = useState(null);


    const fetch = useCallback(() => { ... }, []);


    useEffect(() => { fetch(); }, []);


    return { data, refetch: fetch };
    useEffect(() => {


    const interval = setInterval(handleFocus, cacheTime)


    window.addEventListener('focus', handleFocus, false);


    ...


    return () => {


    clearInterval(interval);


    window.removeEventListener('focus', handleFocus, false);


    ...


    };


    }, []);
    테스트 시간

    View Slide

  17. React
    -
    Query
    QueryClient


    (Context)
    useQuery
    useMutation
    React
    Context API
    useEffect
    useState
    useCallback
    캐싱





    View Slide

  18. React
    -
    Query 없이 React 자체만으로는 불가능하나요?

    View Slide

  19. - React에서 제공하는 기본 Hook으로도 충분히 멋진 비동기 처리 커스텀 훅을 만들 수 있습니다. \😆/


    - 필요한 기능만 구현할 수 있으며 별도의 라이브러리 설치가 필요 없습니다.
    😁


    - 기본 Hook으로 만들기 위해서는 시간과 노력이 필요합니다.
    🤔


    - React
    -
    Query 대비 안정성을 확보하기 어렵습니다.
    😭
    React
    -
    Query 없이 React 자체만으로는 불가능하나요?
    Releases 4 days ago Star 29.9K Download 6.3M / month
    * 2022.09 기준, 출처 : https:/
    /github.com/tanstack/query

    View Slide

  20. React
    -
    Query 조금 알아보기


    Redux에서 React
    -
    Query로 전환 이유


    React
    -
    Query로 전환 과정


    정리

    View Slide

  21. 데이터
    Redux
    구조 구분
    에러
    처리

    View Slide

  22. 데이터
    Redux
    구조 구분
    에러
    처리

    View Slide

  23. Redux
    -
    Saga
    Redux
    상태 관리 라이브러리 미들웨어

    View Slide

  24. Redux
    -
    Saga isLoading
    isSuccess
    isError
    Redux
    Component

    View Slide

  25. loadStatus
    Redux
    -
    Saga isLoading
    isSuccess
    isError
    Redux

    View Slide

  26. React
    -
    Query
    isLoading
    isSuccess
    isError
    기본 옵션!

    View Slide

  27. API
    Actions Sagas
    Reducers

    View Slide

  28. Sagas
    결제 정보 API

    View Slide

  29. Sagas
    결제 정보 API
    사용자 정보 API
    Sagas
    구독 정보 API
    Sagas

    View Slide

  30. Sagas
    결제 정보 API
    사용자 정보 API
    Sagas
    구독 정보 API
    Sagas
    상품 정보 API
    Sagas

    View Slide

  31. Sagas
    결제 정보 API
    사용자 정보 API
    Sagas
    구독 정보 API
    Sagas
    상품 정보 API
    Sagas
    API
    Sagas
    API
    Sagas
    API
    Sagas

    View Slide

  32. Sagas
    결제 정보 API
    사용자 정보 API
    Sagas
    구독 정보 API
    Sagas
    상품 정보 API
    Sagas
    API
    Sagas
    API
    Sagas
    API
    Sagas

    View Slide

  33. Sagas
    결제 정보 API
    사용자 정보 API
    Sagas
    구독 정보 API
    Sagas
    상품 정보 API
    Sagas
    API
    Sagas
    API
    Sagas
    API
    Sagas

    View Slide

  34. 사용자 정보 API
    Query
    구독 정보 API
    Query
    상품 정보 API
    Query
    결제 정보 API
    Query

    View Slide

  35. 사용자 정보 API
    Query
    구독 정보 API
    Query
    상품 정보 API
    Query
    결제 정보 API
    Query
    export function useData({ ...state }) {


    useEffect(() => {


    actions.fetchҳة੿ࠁAPI();


    }, []);


    const { ҳة੿ࠁ } = state;


    return ҳة੿ࠁ;


    }


    export function useData() {


    const { data, isLoading } = useQuery(KEY1, fetch࢚ಿ੿ࠁAPI);


    const { data: ҳة੿ࠁ } = useQuery(


    KEY2,


    () => fetchҳة੿ࠁAPI(data),


    { enabled: !isLoading }


    );


    return ҳة੿ࠁ;


    }
    Redux React
    -
    Query

    View Slide

  36. 데이터
    Redux
    구조 구분
    에러
    처리

    View Slide

  37. 클라이언트 서버
    페이지 이름


    loadStatus
    사용자 정보
    구독 상태








    구독
    정보
    배너
    정보

    View Slide

  38. Redux
    페이지 이름

    loadStatus












    구독
    정보
    배너
    정보

    View Slide

  39. 클라이언트 서버
    페이지 이름


    loadStatus
    사용자 정보
    구독 상태








    구독
    정보
    배너
    정보

    View Slide

  40. Context API


    constate


    recoil


    redux



    React
    -
    Query


    RTK
    -
    Query


    Apollo


    SWR



    … …
    페이지 이름


    loadStatus
    사용자 정보
    구독 상태








    구독
    정보
    배너
    정보

    View Slide


  41. Context API


    constate


    recoil


    redux



    React


    View Slide

  42. 데이터
    Redux
    구조 구분
    에러
    처리

    View Slide

  43. useEffect(() => {


    actions.fetchѾઁ੿ࠁ();


    }, []);


    useEffect(() => {


    if (loadStatus?.fetchѾઁ੿ࠁ?.error) {


    actions.fetching('fetchѾઁ੿ࠁ');


    history.replace("/");


    }


    }, [loadStatus?.fetchѾઁ੿ࠁ?.error]);
    Redux에서의 에러 핸들링

    View Slide

  44. 결제 정보 API
    Redux에서의 에러 핸들링
    컴포넌트
    컴포넌트
    컴포넌트

    View Slide

  45. 에러 발생 loadStatus
    re
    -
    render
    Redux에서의 에러 핸들링
    컴포넌트
    onError


    실행
    결제 정보 API

    View Slide

  46. 에러 발생
    Component
    결제 정보 API


    Query
    onError


    실행
    React
    -
    Query에서의 에러 핸들링

    View Slide

  47. Why?
    “다른 라이브러리 대신 React
    -
    Query를 사용하는 이유가 있나요?”

    View Slide

  48. RTK
    -
    Query Apollo SWR

    View Slide

  49. RTK
    -
    Query Apollo SWR
    “Redux 구조를 그대로 사용해야 합니다.”

    View Slide

  50. RTK
    -

    View Slide

  51. RTK
    -

    View Slide

  52. 다른 라이브러리 대신 React
    -
    Query를 사용하는 이유가 있나요?

    View Slide

  53. - Redux에서 API 상태에 따라 화면을 구성하기 위해서는 별도의 도구나 상태가 필요합니다.
    😭


    - Redux
    -
    Saga는 의존성이 깊은 구조를 만들어 낼 수도 있습니다.
    😢


    - Redux는 간단한 API 추가에도 장황한 Boilerplate가 필요합니다.
    😅


    - Redux는 API 에러 핸들링 과정에서 다소 불필요한 작업이 발생할 수 있습니다.
    🥲
    다른 라이브러리 대신 React
    -
    Query를 사용하는 이유가 있나요?
    - 우수한 비동기 처리 라이브러리가 많이 있습니다. \😆/


    - 사용하는 방식, 구조, 기능에서 React
    -
    Query가 더 적합하다고 판단하여 사용하게 되었습니다.
    🤔

    View Slide

  54. React
    -
    Query 조금 알아보기


    Redux에서 React
    -
    Query로 전환 이유


    React
    -
    Query로 전환 과정


    정리

    View Slide

  55. Redux


    고차 컴포넌트
    Props drilling Production
    My 구독
    React
    -
    Query로 전환 과정

    View Slide

  56. React
    -
    Query로 전환 과정
    Redux 고차 컴포넌트
    ->
    Redux Hook
    Redux Hook
    ->
    React
    -
    Query
    React 16
    ->
    React 18

    View Slide

  57. React
    -
    Query로 전환 과정
    Redux 고차 컴포넌트
    ->
    Redux Hook
    Redux Hook
    ->

    View Slide

  58. 고차 컴포넌트
    전체적인


    코드 구조 변경
    API & 로직 누락


    발생할 수 있음
    Redux 고차 컴포넌트
    ->
    Redux Hook
    불필요한 re
    -
    render


    발생할 수 있음
    Props drilling


    Production



    View Slide

  59. Redux 고차 컴포넌트
    ->
    Redux Hook
    function ࢚ಿ࢚ࣁಕ੉૑({ ঘ࣌, ...࢚క }) {


    useEffect(() => {


    ঘ࣌.fetchҳةؘ੉ఠ({ productGroupId });


    }, [productGroupId]);


    useEffect(() => {


    if (loadStatus?.fetchҳةؘ੉ఠ?.error) {


    error();


    }


    });


    const { ҳةؘ੉ఠ, loadStatus } = ࢚క;


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }


    export default connectStore(࢚ಿ࢚ࣁಕ੉૑);

    View Slide

  60. Redux 고차 컴포넌트
    ->
    Redux Hook
    function ࢚ಿ࢚ࣁಕ੉૑({ ঘ࣌, ...࢚క }) {


    useEffect(() => {


    ঘ࣌.fetchҳةؘ੉ఠ({ productGroupId });


    }, [productGroupId]);


    useEffect(() => {


    if (loadStatus?.fetchҳةؘ੉ఠ?.error) {


    error();


    }


    });


    const { ҳةؘ੉ఠ, loadStatus } = ࢚క;


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }


    export default connectStore(࢚ಿ࢚ࣁಕ੉૑);
    Redux connect 함수

    View Slide

  61. Redux 고차 컴포넌트
    ->
    Redux Hook
    function ࢚ಿ࢚ࣁಕ੉૑({ ঘ࣌, ...࢚క }) {


    useEffect(() => {


    ঘ࣌.fetchҳةؘ੉ఠ({ productGroupId });


    }, [productGroupId]);


    useEffect(() => {


    if (loadStatus?.fetchҳةؘ੉ఠ?.error) {


    error();


    }


    });


    const { ҳةؘ੉ఠ, loadStatus } = ࢚క;


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }


    export default connectStore(࢚ಿ࢚ࣁಕ੉૑);
    dispatch
    selector

    View Slide

  62. Redux 고차 컴포넌트
    ->
    Redux Hook
    function ࢚ಿ࢚ࣁಕ੉૑({ ঘ࣌, ...࢚క }) {


    useEffect(() => {


    ঘ࣌.fetchҳةؘ੉ఠ({ productGroupId });


    }, [productGroupId]);


    useEffect(() => {


    if (loadStatus?.fetchҳةؘ੉ఠ?.error) {


    error();


    }


    });


    const { ҳةؘ੉ఠ, loadStatus } = ࢚క;


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }


    export default connectStore(࢚ಿ࢚ࣁಕ੉૑);

    View Slide

  63. Redux 고차 컴포넌트
    ->
    Redux Hook
    function ࢚ಿ࢚ࣁಕ੉૑({ ঘ࣌, ...࢚క }) {


    useEffect(() => {


    ঘ࣌.fetchҳةؘ੉ఠ({ productGroupId });


    }, [productGroupId]);


    useEffect(() => {


    if (loadStatus?.fetchҳةؘ੉ఠ?.error) {


    error();


    }


    });


    const { ҳةؘ੉ఠ, loadStatus } = ࢚క;


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }


    export default connectStore(࢚ಿ࢚ࣁಕ੉૑);
    selector
    dispatch

    View Slide

  64. Redux 고차 컴포넌트
    ->
    Redux Hook
    function ࢚ಿ࢚ࣁಕ੉૑({ ঘ࣌, ...࢚క }) {


    useEffect(() => {


    ঘ࣌.fetchҳةؘ੉ఠ({ productGroupId });


    }, [productGroupId]);


    useEffect(() => {


    if (loadStatus?.fetchҳةؘ੉ఠ?.error) {


    error();


    }


    });


    const { ҳةؘ੉ఠ, loadStatus } = ࢚క;


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }


    export default connectStore(࢚ಿ࢚ࣁಕ੉૑);

    View Slide

  65. Redux 고차 컴포넌트
    ->
    Redux Hook
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const dispatch = useDispatch();


    useEffect(() => {


    dispatch(actions.fetchҳةؘ੉ఠ({ productGroupId }));


    }, [productGroupId]);


    useEffect(() => {


    // ী۞ ೩ٜ݂


    });


    const ҳةؘ੉ఠ = useSelector(({ ҳةؘ੉ఠ }) => ҳةؘ੉ఠ);


    const loadStatus = useSelector(({ loadStatus }) => loadStatus);


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }

    View Slide

  66. Redux 고차 컴포넌트
    ->
    Redux Hook
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const dispatch = useDispatch();


    useEffect(() => {


    dispatch(actions.fetchҳةؘ੉ఠ({ productGroupId }));


    }, [productGroupId]);


    useEffect(() => {


    // ী۞ ೩ٜ݂


    });


    const ҳةؘ੉ఠ = useSelector(({ ҳةؘ੉ఠ }) => ҳةؘ੉ఠ);


    const loadStatus = useSelector(({ loadStatus }) => loadStatus);


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }
    액션 , 상태 삭제

    View Slide

  67. Redux 고차 컴포넌트
    ->
    Redux Hook
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const dispatch = useDispatch();


    useEffect(() => {


    dispatch(actions.fetchҳةؘ੉ఠ({ productGroupId }));


    }, [productGroupId]);


    useEffect(() => {


    // ী۞ ೩ٜ݂


    });


    const ҳةؘ੉ఠ = useSelector(({ ҳةؘ੉ఠ }) => ҳةؘ੉ఠ);


    const loadStatus = useSelector(({ loadStatus }) => loadStatus);


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }
    useDispatch 추가
    useSelector 추가

    View Slide

  68. Redux 고차 컴포넌트
    ->
    Redux Hook
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const dispatch = useDispatch();


    useEffect(() => {


    dispatch(actions.fetchҳةؘ੉ఠ({ productGroupId }));


    }, [productGroupId]);


    useEffect(() => {


    // ী۞ ೩ٜ݂


    });


    const ҳةؘ੉ఠ = useSelector(({ ҳةؘ੉ఠ }) => ҳةؘ੉ఠ);


    const loadStatus = useSelector(({ loadStatus }) => loadStatus);


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }
    API 요청

    View Slide

  69. loadStatus
    Header Component
    Content Component
    Footer Component
    사용자 정보 API
    매대 정보 API
    상품 정보 API
    Redux Hook 최적화

    View Slide

  70. Redux Hook 최적화
    const fetchࢎਊ੗ؘ੉ఠ = useSelector(({ loadStatus }) => loadStatus.fetchࢎਊ੗ؘ੉ఠ);


    const fetchݒ؀ؘ੉ఠ = useSelector(({ loadStatus }) => loadStatus.fetchݒ؀ؘ੉ఠ);


    const fetch࢚ಿؘ੉ఠ = useSelector(({ loadStatus }) => loadStatus.fetch࢚ಿؘ੉ఠ);
    Redux Hook을 최적화하는 이유


    - 전체적인 구조가 변경됨에 따라 언제 작업이 마무리될지 모릅니다.


    - 작업 도중 다른 작업이 발생할 수 있습니다.


    - 해결할 수 있는 부분을 놔두기가 애매합니다.

    View Slide

  71. Redux Hook 최적화
    const fetchࢎਊ੗ؘ੉ఠ = useSelector(({ loadStatus }) => loadStatus.fetchࢎਊ੗ؘ੉ఠ);


    const fetchݒ؀ؘ੉ఠ = useSelector(({ loadStatus }) => loadStatus.fetchݒ؀ؘ੉ఠ);


    const fetch࢚ಿؘ੉ఠ = useSelector(({ loadStatus }) => loadStatus.fetch࢚ಿؘ੉ఠ);
    export default function useLoadStatus(deps = []) {


    return useSelector(


    ({ loadStatus }) => deps.reduce((status, key) => ({


    ...status, [key]: loadStatus[key]


    }), {}),


    isEqual


    );


    }

    View Slide

  72. Redux Hook 최적화
    const loadStatus = useLoadStatus([‘ fetchࢎਊ੗ؘ੉ఠ ', ‘ fetchݒ؀ؘ੉ఠ ‘,' fetch࢚ಿؘ੉ఠ ‘]);


    export default function useLoadStatus(deps = []) {


    return useSelector(


    ({ loadStatus }) => deps.reduce((status, key) => ({


    ...status, [key]: loadStatus[key]


    }), {}),


    isEqual


    );


    }

    View Slide

  73. Redux Hook 최적화
    const loadStatus = useLoadStatus([‘ fetchࢎਊ੗ؘ੉ఠ ', ‘ fetchݒ؀ؘ੉ఠ ‘,' fetch࢚ಿؘ੉ఠ ‘]);


    0
    19
    38
    기존 useSelector useLoadStatus

    View Slide

  74. Redux Hook 최적화
    const loadStatus = useLoadStatus([‘ fetchࢎਊ੗ؘ੉ఠ ', ‘ fetchݒ؀ؘ੉ఠ ‘,' fetch࢚ಿؘ੉ఠ ‘]);


    0
    19
    38
    기존 useSelector useLoadStatus

    View Slide

  75. Redux Hook
    ->
    React
    -
    Query
    React
    -
    Query로 전환 과정
    Redux 고차 컴포넌트
    ->

    View Slide

  76. Redux Hook
    ->
    React
    -
    Query
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const dispatch = useDispatch();


    useEffect(() => {


    dispatch(actions.fetchҳةؘ੉ఠ({ productGroupId }));


    }, [productGroupId]);


    useEffect(() => {


    if (loadStatus?.fetchҳةؘ੉ఠ?.error) {


    error();


    }


    });


    const ҳةؘ੉ఠ = useSelector(({ ҳةؘ੉ఠ }) => ҳةؘ੉ఠ);


    const loadStatus = useSelector(({ loadStatus }) => loadStatus);


    const fetched = isFetched(deps, loadStatus);


    return fetched && ;


    }

    View Slide

  77. Redux Hook
    ->
    React
    -
    Query
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const { data: ҳةؘ੉ఠ, isLoading } = useQuery(['ҳةؘ੉ఠ'], () => fetchҳةؘ੉ఠ(productGroupId), {


    enabled: productGroupId,


    onError: () => {


    error();


    },


    });


    return !isLoading && ;


    }


    View Slide

  78. Redux Hook
    ->
    React
    -
    Query
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const { data: ҳةؘ੉ఠ, isLoading } = useQuery(['ҳةؘ੉ఠ'], () => fetchҳةؘ੉ఠ(productGroupId), {


    enabled: productGroupId,


    onError: () => {


    error();


    },


    });


    return !isLoading && ;


    }


    loadStatus 대신 사용

    View Slide

  79. Redux Hook
    ->
    React
    -
    Query
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const { data: ҳةؘ੉ఠ, isLoading } = useQuery(['ҳةؘ੉ఠ'], () => fetchҳةؘ੉ఠ(productGroupId), {


    enabled: productGroupId,


    onError: () => {


    error();


    },


    });


    return !isLoading && ;


    }


    Dispatch & Selector 대신 사용

    View Slide

  80. Redux Hook
    ->
    React
    -
    Query
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const { data: ҳةؘ੉ఠ, isLoading } = useQuery(['ҳةؘ੉ఠ'], () => fetchҳةؘ੉ఠ(productGroupId), {


    enabled: productGroupId,


    onError: () => {


    error();


    },


    });


    return !isLoading && ;


    }


    에러 핸들링

    View Slide

  81. Redux Hook
    ->
    React
    -
    Query
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const { data: ҳةؘ੉ఠ, isLoading } = useQuery(['ҳةؘ੉ఠ'], () => fetchҳةؘ੉ఠ(productGroupId), {


    enabled: productGroupId,


    onError: () => {


    error();


    },


    });


    return !isLoading && ;


    }


    with React 16

    View Slide

  82. Redux Hook
    ->
    React
    -
    Query
    export default function ࢚ಿ࢚ࣁಕ੉૑() {


    const { data: ҳةؘ੉ఠ, isLoading } = useQuery(['ҳةؘ੉ఠ'], () => fetchҳةؘ੉ఠ(productGroupId), {


    enabled: productGroupId,


    onError: () => {


    error();


    },


    });


    return !isLoading && ;


    }


    with React 16
    if (!ݒ؀Success || !࢚ಿSuccess || (!੉ਊӂIdle && !੉ਊӂSuccess) || ੌद઺૑࢚ಿLoading) {


    return null;


    }

    View Slide

  83. React
    -
    Query 개선 - useQueriesLoading
    export function useQueriesLoading() {


    const client = useQueryClient();


    const queries = client.getQueryCache().findAll();


    const [queryChangedCount, setQueryChangedCount] = useState(0);


    useEffect(() => {


    if (queries) {


    setQueryChangedCount(queries.length);


    }


    }, []);


    useEffect(() => {


    if (queries.every(({ state }) => state.status !== 'loading') && queryChangedCount >= 0) {


    setQueryChangedCount((prevState) => prevState - 1);


    }


    });


    return queryChangedCount >= 0


    }


    View Slide

  84. React
    -
    Query 개선 - useQueriesLoading
    export function useQueriesLoading() {


    const client = useQueryClient();


    const queries = client.getQueryCache().findAll();


    const [queryChangedCount, setQueryChangedCount] = useState(0);


    useEffect(() => {


    if (queries) {


    setQueryChangedCount(queries.length);


    }


    }, []);


    useEffect(() => {


    if (queries.every(({ state }) => state.status !== 'loading') && queryChangedCount >= 0) {


    setQueryChangedCount((prevState) => prevState - 1);


    }


    });


    return queryChangedCount >= 0


    }


    등록된 쿼리 가져오기

    View Slide

  85. React
    -
    Query 개선 - useQueriesLoading
    export function useQueriesLoading() {


    const client = useQueryClient();


    const queries = client.getQueryCache().findAll();


    const [queryChangedCount, setQueryChangedCount] = useState(0);


    useEffect(() => {


    if (queries) {


    setQueryChangedCount(queries.length);


    }


    }, []);


    useEffect(() => {


    if (queries.every(({ state }) => state.status !== 'loading') && queryChangedCount >= 0) {


    setQueryChangedCount((prevState) => prevState - 1);


    }


    });


    return queryChangedCount >= 0


    }


    전체 쿼리 개수를 state 저장

    View Slide

  86. React
    -
    Query 개선 - useQueriesLoading
    export function useQueriesLoading() {


    const client = useQueryClient();


    const queries = client.getQueryCache().findAll();


    const [queryChangedCount, setQueryChangedCount] = useState(0);


    useEffect(() => {


    if (queries) {


    setQueryChangedCount(queries.length);


    }


    }, []);


    useEffect(() => {


    if (queries.every(({ state }) => state.status !== 'loading') && queryChangedCount >= 0) {


    setQueryChangedCount((prevState) => prevState - 1);


    }


    });


    return queryChangedCount >= 0


    }


    loading 상태인지 확인

    View Slide

  87. React
    -
    Query 개선 - useQueriesLoading
    if (!ݒ؀Success || !࢚ಿSuccess || (!੉ਊӂIdle && !੉ਊӂSuccess) || ੌद઺૑࢚ಿLoading) {


    return null;


    }
    const isQueriesLoading = useQueriesLoading();


    if(isQueriesLoading){


    return null


    }

    View Slide

  88. Project
    새로운 기능
    리팩터링

    View Slide

  89. 폴더 구조 개선
    Query


    Mutation


    네이밍
    Query


    작성 방식
    프로젝트


    기본 옵션
    Query Key


    작성 규칙

    View Slide

  90. Query


    작성 방식
    폴더 구조 개선

    View Slide

  91. Query


    작성 방식
    폴더 구조 개선

    View Slide

  92. Query 작성 방식
    사용자 정보 API
    매대 정보 API
    상품 정보 API
    Header Component
    Content Component
    Footer Component

    View Slide

  93. Query 작성 방식
    상품 정보 API
    Header Component
    Content Component

    View Slide

  94. API
    Query 작성 방식
    기본 Query


    (기본 Mutation)

    View Slide

  95. 기본 Query


    (기본 Mutation)
    Query 작성 방식
    export function Component() {


    const { data: ࢚ಿ੿ࠁ, isLoading } = use࢚ಿ੿ࠁ();


    const { data: ҳة੿ࠁ } = useҳة੿ࠁ({ ࢚ಿ੿ࠁ, options: {


    enabled: !isLoading


    }});


    return ;


    }
    export function useData() {


    const { data: ࢚ಿ੿ࠁ, isLoading } = use࢚ಿ੿ࠁ();


    const { data: ҳة੿ࠁ } = useҳة੿ࠁ({ ࢚ಿ੿ࠁ, options: {


    enabled: !isLoading


    }});


    return ҳة੿ࠁ;


    }
    컴포넌트 커스텀 훅

    View Slide

  96. Query 작성 방식
    기본 Query


    (기본 Mutation)
    공통 옵션
    +

    View Slide

  97. Query 작성 방식
    기본 Query


    (기본 Mutation)
    +
    공통 옵션
    useErrorBoundary
    select

    View Slide

  98. 기본 Query를 위한 기본 Query
    Query 작성 방식
    +
    useQuery 공통 옵션

    View Slide

  99. Query 작성 방식
    - 페이지 단위 Key 값 추가


    - Select에서 실질적인 데이터가 있는 값만 반환


    - onError 옵션이 있어야 useErrorBoundary를 설정하도록 처리
    import { useQuery as useQueryOrigin } from 'react-query';
    export default function useQuery(queryKey, queryFn, options = {}) {
    const { … } = options;
    return useQueryOrigin(queryKey, queryFn, { … });
    }
    ->
    페이지 단위 쿼리 초기화 가능
    ->
    쿼리를 사용할 때 중복 Select 방지
    ->
    에러 핸들링 누락 방지

    View Slide

  100. Query


    View Slide

  101. Redux + Redux
    -
    Saga
    폴더 구조 개선
    - data


    - 매대데이터


    - actions


    - actionTypes


    - reducers


    - sagas


    - services


    - 홈데이터
    React
    -
    Query
    - hooks


    - services


    - queries


    - 매대데이터


    - 홈데이터


    - mutations

    View Slide

  102. 폴더 구조 개선
    actions actionTypes reducers sagas services
    export function useݒ؀({ options } = {}) {


    return useQuery([queryKeys.ݒ؀], () => api.get(`/ݒ؀/API`), options);


    }
    action actionType reducer saga service

    View Slide

  103. 폴더 구조 개선
    actions actionTypes reducers sagas services
    export function useݒ؀({ options } = {}) {


    return useQuery([queryKeys.ݒ؀], () => api.get(`/ݒ؀/API`), options);


    }
    action actionType reducer saga service

    View Slide

  104. 폴더 구조 개선
    Redux + Redux
    -
    Saga
    - data


    - 매대데이터


    - actions


    - actionTypes


    - reducers


    - sagas


    - services


    - 홈데이터

    View Slide

  105. 폴더 구조 개선
    React
    -
    Query
    - hooks


    - services


    - queries


    - 매대데이터


    - 홈데이터


    - mutations

    View Slide

  106. 폴더 구조 개선
    React
    -
    Query
    - hooks


    - services


    - queries


    - 매대데이터


    - 홈데이터


    - mutations
    POST, DELETE …
    GET

    View Slide

  107. 폴더 구조 개선
    - hooks


    - services


    - queries


    - 매대데이터


    - 홈데이터


    - mutations
    React
    -
    Query

    View Slide

  108. 폴더 구조 개선
    - hooks


    - services


    - queries


    - 매대데이터


    - 홈데이터


    - mutations
    Page
    Component
    Hook
    React
    -
    Query

    View Slide

  109. My구독
    중복 API 및 로직 최소화
    API 네이밍

    View Slide

  110. My구독
    중복 API 및 로직 최소화
    API 네이밍
    import 경로에서 목적 표현 import { useݒ؀ؘ੉ఠ } from '@/hooks/queries/ݒ؀ؘ੉ఠ';
    목적

    View Slide

  111. My구독
    중복 API 및 로직 최소화
    API 네이밍
    import 경로에서 목적 표현
    동일한 API 요청 최소화

    View Slide

  112. React
    -
    Query로 전환 과정
    React 16
    ->
    React 18
    Redux 고차 컴포넌트
    -

    View Slide

  113. Safari
    Chrome Internet Explorer
    React 16
    ->
    React 18

    View Slide

  114. Safari
    Chrome
    React 16
    ->
    React 18
    Internet Explorer

    View Slide

  115. React
    -
    Query v4


    (TanStack Query v4)
    React 18
    React 16
    ->
    React 18

    View Slide

  116. Suspense

    View Slide

  117. React 16
    ->
    React 18
    function ࢚ಿ࢚ࣁۨ੉ইਓ() {


    ...


    const { data } = useQuery(...);


    return ;


    }
    function ࢚ಿ࢚ࣁಕ੉૑() {


    ...


    const { isSuccess } = useQuery(...);


    const { isSuccess: ҳة੿ࠁSuccess } = useQuery(...);


    const isRender = isSuccess && ҳة੿ࠁSuccess;


    return isRender && <࢚ಿ࢚ࣁۨ੉ইਓ>࢚ಿ࢚ࣁۨ੉ইਓ>;


    }
    Redux
    데이터 바탕으로 화면 구성
    데이터 요청 및 렌더링 여부 결정

    View Slide

  118. function ࢚ಿ࢚ࣁۨ੉ইਓ() {


    ...


    const { data } = useQuery(...);


    return ;


    }
    function ࢚ಿ࢚ࣁಕ੉૑() {


    ...


    const { isSuccess } = useQuery(...);


    const { isSuccess: ҳة੿ࠁSuccess } = useQuery(...);


    const isRender = isSuccess && ҳة੿ࠁSuccess;


    return isRender && <࢚ಿ࢚ࣁۨ੉ইਓ>࢚ಿ࢚ࣁۨ੉ইਓ>;


    }
    중복 요청 re
    -
    render
    React 16
    ->
    React 18

    View Slide

  119. React 16
    ->
    React 18
    function ࢚ಿ࢚ࣁۨ੉ইਓ() {


    ...


    const { data } = useQuery(...);


    return ;


    }
    function ࢚ಿ࢚ࣁಕ੉૑() {


    ...


    const { isSuccess } = useQuery(...);


    const { isSuccess: ҳة੿ࠁSuccess } = useQuery(...);


    const isRender = isSuccess && ҳة੿ࠁSuccess;


    return isRender && <࢚ಿ࢚ࣁۨ੉ইਓ>࢚ಿ࢚ࣁۨ੉ইਓ>;


    }

    View Slide

  120. React 16
    ->
    React 18
    function ࢚ಿ࢚ࣁಕ੉૑() {


    ...


    return <࢚ಿ࢚ࣁۨ੉ইਓ />;


    }
    function Route() {


    ...


    return <࢚ಿ࢚ࣁಕ੉૑ />;


    }
    function ࢚ಿ࢚ࣁۨ੉ইਓ() {


    ...


    const { data } = useQuery(...);


    return ;


    }
    isRender 분기 삭제

    View Slide

  121. React 16
    ->
    React 18
    function ࢚ಿ࢚ࣁಕ੉૑() {


    ...


    const { data } = useQuery(...);


    return ;


    }
    function Route() {


    ...


    return <࢚ಿ࢚ࣁಕ੉૑ />;


    }

    View Slide

  122. React 16
    ->
    React 18
    function ࢚ಿ࢚ࣁಕ੉૑() {


    ...


    const { data } = useQuery(...);


    return ;


    }
    function Route() {


    ...


    return <࢚ಿ࢚ࣁಕ੉૑ />;


    }
    function ࢚ಿ࢚ࣁۨ੉ইਓ() {


    ...


    const { data } = useQuery(...);


    return ;


    }
    function ࢚ಿ࢚ࣁಕ੉૑() {


    ...


    const { isSuccess } = useQuery(...);


    const { isSuccess: ҳة੿ࠁSuccess } = useQuery(...);


    const isRender = isSuccess && ҳة੿ࠁSuccess;


    return isRender && <࢚ಿ࢚ࣁۨ੉ইਓ>࢚ಿ࢚ࣁۨ੉ইਓ>;


    }
    이전 이후

    View Slide

  123. Redux에서 React
    -
    Query로 전환 과정

    View Slide

  124. - My구독 특성과 상황 덕분에 총 3단계로 나누어 전환작업을 진행했습니다.


    - 과정마다 그 상황에 맞춰 최대한 더 좋은 방향으로 진행할 수 있도록 여러 도전을 진행했습니다.


    - Redux Hook 기반으로 전환할 때 select를 좀 더 최적화하는 방법에 대해 공유했습니다.


    - React
    -
    Query로 전환하는 과정에서 React 16에서 상태 지옥에서 벗어나는 방법에 대해 공유했습니다.


    - My구독에서 React
    -
    Query를 어떻게, 어떤 구조로 사용하는지 공유했습니다.


    - React 18로 전환하면서 어떤 이점이 발생하는지 공유했습니다.
    Redux에서 React
    -
    Query로 전환 과정

    View Slide

  125. React
    -
    Query 조금 알아보기


    Redux에서 React
    -
    Query로 전환 이유


    React
    -
    Query로 전환 과정


    정리

    View Slide

  126. 그래서 써보니 어떤가요?

    View Slide

  127. 이런 점에서는 좋았습니다. \😆/
    - Hook 기반을 제공하고 캐싱을 지원합니다.

    View Slide

  128. 이런 점에서는 좋았습니다. \😆/
    - Hook 기반을 제공하고 캐싱을 지원합니다.


    - 비동기 처리를 위한 유용한 도구를 제공합니다.

    View Slide

  129. 이런 점에서는 좋았습니다. \😆/
    - Hook 기반을 제공하고 캐싱을 지원합니다.


    - 비동기 처리를 위한 유용한 도구를 제공합니다.
    비동기 처리 상태
    isLoading


    isFetching


    isSuccess



    In
    fi
    nite Queries

    View Slide

  130. 이런 점에서는 좋았습니다. \😆/
    - Hook 기반을 제공하고 캐싱을 지원합니다.


    - 비동기 처리를 위한 유용한 도구를 제공합니다.
    In
    fi
    nite Queries
    My구독
    화면 render 여부


    Query enabled 처리
    결제 내역 화면
    비동기 처리 상태

    View Slide

  131. 이런 점에서는 좋았습니다. \😆/
    - Hook 기반을 제공하고 캐싱을 지원합니다.


    - 비동기 처리를 위한 유용한 도구를 제공합니다.


    - 백그라운드 패칭을 지원합니다.
    Background Fetching
    focus,


    mount,


    interval



    View Slide

  132. 이런 점에서는 좋았습니다. \😆/
    - Hook 기반을 제공하고 캐싱을 지원합니다.


    - 비동기 처리를 위한 유용한 도구를 제공합니다.


    - 백그라운드 패칭을 지원합니다.
    My구독
    이용권 등록 및 변경 페이지


    구독 상세 페이지



    Background Fetching

    View Slide

  133. 이런 점에서는 좋았습니다. \😆/
    - Hook 기반을 제공하고 캐싱을 지원합니다.


    - 비동기 처리를 위한 유용한 도구를 제공합니다.


    - 백그라운드 패칭을 지원합니다.
    게시판 댓글 좋아요 / 싫어요 주식 목록

    View Slide

  134. 이런 상황에서는 고민을 해보세요. 🤔
    - 서버 사이드 데이터가 거의 없는 경우 recoil


    redux



    View Slide

  135. 이런 상황에서는 고민을 해보세요. 🤔
    - 서버 사이드 데이터가 거의 없는 경우
    ->
    서버 사이드 데이터가 더 많아질 때 적용

    View Slide

  136. - React 18에서 비동기 처리 상태때문에 도입하는 경우
    이런 상황에서는 고민을 해보세요. 🤔
    - 서버 사이드 데이터가 거의 없는 경우
    ->
    Suspense 사용

    View Slide

  137. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.
    Redux
    -
    Saga의 takeLatest

    View Slide

  138. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.
    function useExecutionOnce(func, delay) {


    ...


    return async (...args) => {


    if (!completeRef.current) {


    return;


    }


    completeRef.current = false;


    try {


    await func(...args);


    await waitMutating();


    } catch (e) {


    throw e;


    }


    };


    }

    View Slide

  139. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.
    function useExecutionOnce(func, delay) {


    ...


    return async (...args) => {


    if (!completeRef.current) {


    return;


    }


    completeRef.current = false;


    try {


    await func(...args);


    await waitMutating();


    } catch (e) {


    throw e;


    }


    };


    }
    비동기 작업 대기

    View Slide

  140. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.
    function useExecutionOnce(func, delay) {


    ...


    return async (...args) => {


    if (!completeRef.current) {


    return;


    }


    completeRef.current = false;


    try {


    await func(...args);


    await waitMutating();


    } catch (e) {


    throw e;


    }


    };


    }
    useIsMutaing 기반 mutation 작업 대기

    View Slide

  141. Storybook
    이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.
    My구독
    E2E Test
    Snapshot Test

    View Slide

  142. Storybook
    이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.
    My구독
    E2E Test
    Snapshot Test
    Mock Service Worker

    View Slide

  143. 구독 완료 페이지
    이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.


    - 항상 서버 데이터와 같은 데이터를 바라보는 것이 좋은 것은 아닙니다.
    1회성 페이지 에러
    재요청

    View Slide

  144. 구독 완료 페이지
    이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.


    - 항상 서버 데이터와 같은 데이터를 바라보는 것이 좋은 것은 아닙니다.
    1회성 API
    refetchOnWindowFocus: false,


    retry: 0,

    View Slide

  145. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.


    - 항상 서버 데이터와 같은 데이터를 바라보는 것이 좋은 것은 아닙니다.


    - 전체적인 데이터 흐름을 파악하기 어렵다는 느낌을 받기도 합니다.
    Pure 컴포넌트 상태 컴포넌트

    View Slide

  146. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.


    - 항상 서버 데이터와 같은 데이터를 바라보는 것이 좋은 것은 아닙니다.


    - 전체적인 데이터 흐름을 파악하기 어렵다는 느낌을 받기도 합니다.
    Pure 컴포넌트
    상태 컴포넌트
    API 데이터 중 하나만 필요

    View Slide

  147. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.


    - 항상 서버 데이터와 같은 데이터를 바라보는 것이 좋은 것은 아닙니다.


    - 전체적인 데이터 흐름을 파악하기 어렵다는 느낌을 받기도 합니다.
    Pure 컴포넌트 상태 컴포넌트
    Props drilling
    API 데이터 중 하나만 필요

    View Slide

  148. 이런 아쉬운 점이 있습니다. 😭
    - Mutation은 한 번만 호출하기 위해 Wrapper 함수가 필요합니다.


    - UI 테스트를 진행하게 될 때 Mock API가 필요할 수 있습니다.


    - 항상 서버 데이터와 같은 데이터를 바라보는 것이 좋은 것은 아닙니다.


    - 전체적인 데이터 흐름을 파악하기 어렵다는 느낌을 받기도 합니다.

    View Slide

  149. 보이지 않는 개선
    My구독의 Redux에서 React
    -
    Query 전환 경험 공유


    View Slide

  150. 참고 자료 & 이미지 출처
    21) React-Query ܾܻૉ, Star, ׮਍۽٘ ੿ࠁ https://github.com/tanstack/query


    49, 50, 51, 52, 53) ࠺زӝ ࢚క ҙܻ ۄ੉࠳۞ܻ ࠺Ү https://react-query-v3.tanstack.com/comparison


    51) RTK-Query ੿ࠁ https://redux-toolkit.js.org/rtk-query/overview


    52) apollo ੿ࠁ https://www.apollographql.com/


    53) SWR ੿ࠁ https://swr.vercel.app/ko


    90-94) useQueriesLoading ղਊ https://tech.kakao.com/2022/06/13/react-query/


    120, 121, 122) IE, Chrome, Safari, React ই੉௑ https://commons.wikimedia.org/


    122) React-Query ই੉௑ https://react-query-v3.tanstack.com/


    151) Mock Service Worker ই੉௑ https://mswjs.io/

    View Slide

  151. E.O.D

    View Slide