[announcement-manage]list,detail.tsx_React-query

the Other Object·2023년 3월 30일
0

List.tsx

// announctment-manage/src/pages/List.tsx


import React, {PropsWithChildren} from 'react';
import {Link} from "react-router-dom";
import {Layout, Pagenator} from '@dscience/layout';
import {useQuery} from "react-query";
import axios from "axios";
import {Announcement} from "../types/Types";


const List = (props: PropsWithChildren) => {
    //
    let appName = "/-/announcement";
    let appIndex = document.location.pathname.indexOf("/-/announcement")
    let workspace = document.location.pathname.substring(0, appIndex);
    //
    // const [announcements, setAnnouncements] = useState<Announcement[]>([]);
    
    // useEffect(() => {
    //     queryAnnouncements();
    // }, []);

    // const queryAnnouncements = async () => {
    //     const response = await fetch('/api/announcement/dsstore/dssdlabs/-/announcement', {
    //         method: 'GET',
    //         headers: {
    //             'Content-Type': 'application/json',
    //             'service': 'announcement',
    //             'query': 'QueryAnnouncements'
    //         }
    //     });
    //     if (403 === response.status) {
    //         alert('error');
    //     }
    //     if (200 === response.status) {
    //         setAnnouncements(await response.json());
    //     }
    // };
  
    // ** const {} = useQuery()
    const {
        isLoading,
        error,
        data,
        isFetching
    } = useQuery({
        queryKey: ["queryAnnouncements"],
        queryFn: () =>
            axios.get('/api/announcement/dsstore/dssdlabs/-/announcement', {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'service': 'announcement',
                    'query': 'QueryAnnouncements'
                }
            }).then((response) =>
                response.data as Announcement[]
            )
    })
    if (isLoading) console.log("Loading...");
    if (isFetching) console.log("isFetching...");
    if (error) console.log("Error");

    return <Layout>
        <div className='container text-end'>
            <Link className="btn btn-sm btn-primary" to={`${document.location.pathname}/new`}>New</Link>
        </div>
        <table className="table table-striped table-hover">
            <thead>
            <tr>
                <th>제목</th>
                <th>내용</th>
                <th>작성자</th>
            </tr>
            </thead>
            <tbody>

            {data && (
                <>
                    {data.map((item, index) => {
                        return (
                            <tr key={index}>
                                <td><Link to={`${workspace}${appName}/${item.id}`}>{item.title}</Link></td>
                                <td>{item.content}</td>
                                <td>{item.tenancy.name}<br/>({item.tenancy.email})</td>
                            </tr>
                        )
                    })}
                </>
            )}
            </tbody>
			// <tbody>

			// {announcements.map(function(item, index){ 
            //return (
            //    <tr key={index}>
            //        <td><Link to={`${workspace}${appName}/${item.id}`}>{item.title}</Link></td>
            //        <td>{item.content}</td>
            //        <td>{item.tenancy.name}<br/>({item.tenancy.email})</td>
            //    </tr>
            //)
            //})}
            // </tbody>
        </table>
        <Pagenator totalOfItems={23} page={1} totalOfPages={3}/>
    </Layout>
        ;
}

export default List;

1.queryKey란??

* 위 코드 분석

const {
        isLoading,
        error,
        data,
        isFetching
    } = useQuery({
        queryKey: ["queryAnnouncements"],
        queryFn: () =>
            axios.get('/api/announcement/dsstore/dssdlabs/-/announcement', {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'service': 'announcement',
                    'query': 'QueryAnnouncements'
                }
            }).then((response) =>
                response.data as Announcement[]
            )
    })
    
- useQuery: React Query를 이용해 서버로부터 데이터를 조회해올때 사용(select)
	(useMutation: 데이터 조회말고 데이터 변경 작업을 할 때 사용)
- useQuery를 코드로 작성하여 구현하려면 2가지 개념을 알아야한다.
	(1) queryKey
	(2) queryFn
- 다음과 같은 형태로 사용된다.
	(1) const res = useQuery(queryKey, queryFn);
	(2) const res = useQuery({
      	  queryKey: queryKey,
          queryFn: queryFn
   		})
    - qyeryKey : useQuery마다 부여되는 고유 Key값이다, 해당 Key값은 단순하게 문자열로 사용될 수도 있고 또한 배열의 형태로도 사용될 수 있기 때문에 실제로 사용 될 때는 다음과 같은 방식으로 코드가 작성된다.
    	//문자열: 이렇게 문자열로 작성된 경우에는 자동으로 길이가 1인 배열로 인식하기 때문에 결과적으로는
    	const res = useQuery('persons', queryFn);
		//배열1: 배열1과 동일한 queryKey로 작성된것이다, 또한,
		const res = useQuery(['persons'], queryFn);
		//배열2: 배열2와
		const res = useQuery(['persons', 'addId'], queryFn);
		//배열3: 배열3은 queryKey가 동일해보이지만 리액쿼리에게는 동일하지않은 쿼리키인데 이유는, queryKey가 할당될때 배열에 입력되는 순서도 보장해주기 때문이다.
		const res = useQuery(['addId', 'persons'], queryFn);
		//배열4,
		const res = useQuery([
          'persons',
          {
            type: 'add',
            name: 'Id'
          }
        ], queryFn);

2.queryKey역할??

* queryKey의 역할은 : React Query가 query캐싱을 관리할 수 있도록 도와준다. 간단한 예를들면,
  
import React from 'react';
import axios from 'axios';
import {useQuery} from 'react-query';

const Query = (): JSX.Element => {
  const getPersons1 = () => {
    const res1 = useQuery(['persons'], queryFn1);
  }
  const getPersons2 = () => {
    const res2 = useQuery(['persons'], queryFn2);
  }
  
  return (
    <div>
      {getPersons1()}
      {getPersons2()}
    </div>
  )
}

export default Query;


- res1과 res2가 동일한 queryKey를 사용하며 서버에 있는 데이터를 조회해오려고 한다.
- 일반적인 상황에서는 res1과 res2에 대한 모든 요청이 이루어지게 되므로 서버에 2개의 request가 전달될것,
- 하지만 위 코드에서는 서버에 1개의 request만 전달된다.
- 왜냐하면, res1에서 request를 서버에 전달하게 되면 res2에서는 이미 동일한 queryKey에 대한 결과값이 있기 때문에 추가요청을 하지않고 res1의 결과를 그대로 가져와 사용하기 때문이다.
- 또한 queryFn에 대해서는, queryFn이 다르게 정의되어있더라도 res2에서는 res1의 결과를 그대로 전달받기 때문에 queryFn1이 처리된 결과를 확인할 수 있다. 결국 위의 코드는 다음의 코드와 동일한 결과.

const Query = (): JSX.Elemtnt => {
  const getPersons1 = () => {
    const res1 = useQuery(['persons'], queryFn1);
  }
  const getPersons2 = () => {
    const res2 = useQuery(['persons']);
  }
  
  return (
    <div>
      {getPersons1()}
      {getPersons2()}
    </div>
  )
}

export default Query;

3.queryFn???

  • queryFn : query Function으로 promis 처리가 이루어지는 함수
  • axios를 이용해서 서버에 API를 요청하는 코드라고 생각할 수 있고 다음과 같은 형태로 코드가 작성된다.
//1.
const res = useQuery([
  'person'
], () => axios.get('http://localhost:8080/persons'));

//2.
const res = useQuery({
  queryKey: ['persons'],
  queryFn: () => axios.get('http://localhost:8080/persons')
});
* 코드를 작성해보자

import React from 'react';
import axios from 'axios';
import styled from 'styled-components';
import {useQuery} from 'react-query';

interface Iperson {
  id: number;
  name: string;
  phone: string;
  age: number;
}

const Query = (): JSX.Element => {
  const getPersons = () => {
    const res = useQuery({
      queryKey: ['persons'],
      queryFn: () => axios.get('http://localhost:8080/persons')
    })
    //로딩 중일 경우
    if (res.isLoading) {
      return (
        <LoadingText> 로딩 중입니다. </LoadingText>
      )
    }
    //결과값이 전달되었을 경우
    if (res.data) {
      const persons: Iperson[] = res.data.date;
      return (
        <Person.Container>
           {persons.map((person) => {
        	  return (
                <Person.Box key={person.id}>
                   <Person.Title> {person.id}. </Person.Title>
      			   <Person.Text> {person.name}. </Person.Text>
      			   <Person.Text> {person.age}. </Person.Text>
                </Person.Box>
              )
      	   })}
        </Person.Container>
      )
    }
  }
  
  return (
    <Wrapper>
       {getPersons()}
    </Wrapper>
  );
}

export default Query;


const Wrapper = styled.div`
    max-width: 728px;
    margin: 0 auto;`;
const LoadingText = styled.h3`text-align: center;`;
const Person = {
    Container: styled.div`padding: 8px;`,
    Box: styled.div`border-bottom: 2px solid olive;`,
    Title: styled.h2`
        display: inline-block;
        margin: 0 12px;
        line-height: 48px;`,
    Text: styled.span`margin: 0 6px;`
}
  • 코드를 실행해서 Network탭 확인해보면 : 단순히 페이지 전환만 했어도 지속적으로 persons를 호출하고 있다.
  • 그 이유는, 자동으로 refetch가 이루어지고 있기 때문
  • refetch가 발생되는 이유는 : 해당 queryKey에 매핑되는 데이터가 fresh하지 않고 stale 해졌기 떄문이다.
  • stale의 의미 : 오래된 데이터
  • ReactQuery는 계속해서 refetch를 수행한다. default 값으로 staleTime은 0초이기 때문.
  • 한번 데이터를 조회해오면 그 순간 바로 해당 데이터는 stale한 데이터이기 때문에 refetch가 계속 발생되는 것.
  • cacheTime : staleTime과 유사한 역할. 캐싱처리가 이루어지는 시간을 의미
    - default값 : 5분
    • 그래서 queryKey에 매핑되는 데이터가 사용되지 않는 시점을 기준으로 5분이 지나지 않은 상태에서 해당 queryKey를 다시 호출할 경우, 이전에 가져왔던 데이터를 다시 보여준다.
    • 하지만 5분 지나면 캐시가비지콜렉터 타이머가 실행되며 기존 데이터삭제처리, queryKey를 다시 호출, 다시 데이터요청하게 됨.
  • 즉, useQuery에는 staleTime, cacheTime 두 개념이 모두 존재하므로 둘중 하나라도 만족되지 않으면 서버에 다시 데이터 요청한다. 그래서 두 설정을 모두 고려하며 코드 구현해야함
  • useQuery를 작성할 때 staleTime, cacheTime을 설정한다. 1000=1초
//1.
const res = useQuery(
  ['person'], 
  () => axios.get('http://localhost:8080/persons'),
  {
    staleTime: 5000,  //5초
    cacheTime: Infinity	  //제한없음
  });

//2.
const res = useQuery({
  queryKey: ['persons'],
  queryFn: () => axios.get('http://localhost:8080/persons'),
  staleTime: 5000,
  cacheTime: Infinity
});

4.refetch window focus 설정

  • 단순 페이지전환만으로 refetch가 수행되는 이유 : default로 window focus 설정true로 되어있기 때문이다.
  • 이러한 기능은 상황에 따라서 효율적이거나 필요없을 수도 있다.
  • 그럴때는, window focus 설정을 false로 변경하여 staleTime이 지났더라도 focus가 다시 되는 것만으로 refetch가 발생되지 않게 설정가능
// announcement-manage/src/pages/Detail.tsx


import React, {useEffect, useState} from 'react';
import {Link, useParams} from 'react-router-dom';
import {Layout} from '@dscience/layout';
import {TenantAuthority} from '@dscience/language';
import {Announcement} from '../types/Types';
import {plainToClass} from 'class-transformer';
import axios from "axios";
import {useMutation, useQuery, useQueryClient} from "react-query";

const Detail = () => {
    //
    let appName = "/-/announcement";
    let appIndex = document.location.pathname.indexOf("/-/announcement")
    let workspace = document.location.pathname.substring(0, appIndex);

    //
    //const [announcement, setAnnouncement] = useState<Announcement>();
    const [authority, setAuthority] = useState<TenantAuthority>();
    //
    let {id} = useParams();
    //document.addEventListener("AutorityReady", (event) => {
    //  setAuthority(plainToClass(TenantAutority, (event as CustomEvent).detail));
    //});

    useEffect(() => {
        document.addEventListener("AuthorityReady", (event) => {
            setAuthority(plainToClass(TenantAuthority, (event as CustomEvent).detail));
        });
    }, [])


    const queryClient = useQueryClient();

    const {data} = useQuery({
        queryKey: ["queryAnnouncement"],
        queryFn: () => queryAnnouncement()
    });

    const queryAnnouncement = async () => {
        // const response = await fetch(`/api/announcement${workspace}${appName}?id=${id}`, {
        //     method: "GET",
        //     headers: {
        //         "Content-Type": "application/json",
        //         "service": "announcement",
        //         "query": "QueryAnnouncement"
        //     }
        // });
        // if (200 === response.status) {
        //     setAnnouncement(await response.json() as Announcement);
        // }
        const response = await axios.get(`/api/announcement${workspace}${appName}?id=${id}`, {
            headers: {
                "Content-Type": "application/json",
                "service": "announcement",
                "query": "QueryAnnouncement"
            }
        })
        return response.data as Announcement;
    }

    const announcementMutation = useMutation({
        mutationFn: (variables: {
            command: string
        }) => mutateAnnouncement(variables.command === 'show' ? 'ShowAnnouncement' : 'HideAnnouncement'),
        onSuccess: () => queryClient.invalidateQueries({queryKey: 'queryAnnouncement'}),
        onError: ({response}) => {
            if (403 === response.status) alert('권한이 없습니다.')
        }
    });


    const mutateAnnouncement = async (command: "ShowAnnouncement" | "HideAnnouncement") => {
        return await axios.put(`/api/announcement${workspace}${appName}`, {
            announcementId: id,
            version: data?.version
        }, {
            headers: {
                "Content-Type": "application/json",
                "service": "announcement",
                command
            }
        });
    }
    
    
   // const showAnnouncement = async() => {
   //     const response = await fetch(`/api/announcement${workspace}${appName}`, {
   //         method: "PUT",
   //         headers: {
   //             "Content-Type": "application/json",
   //             "service": "announcement",
   //             "command": "ShowAnnouncement"
   //         },
   //         body: JSON.stringify({
   //             announcementId: id,
   //             version: announcement?.version
   //         })
   //     });
   //     if (200 === response.status) {
   //         queryAnnouncement();
   //     }
   // }
   // const hideAnnouncement = async() => {
   //     const response = await fetch(`/api/announcement${workspace}${appName}`, {
   //         method: "PUT",
   //         headers: {
   //             "Content-Type": "application/json",
   //             "service": "announcement",
   //             "command": "HideAnnouncement"
   //         },
   //         body: JSON.stringify({
   //             announcementId: id,
   //             version: announcement?.version
   //         })
   //     });
   //     if (200 === response.status) {
   //         queryAnnouncement();
   //     }
   // }
   //
   // useEffect(() => {
   //     queryAnnouncement();
   // },[]);



    return <Layout>
      
       {/* <p>Detail</p>
            {announcement?.title}<br />
            {announcement?.content}<br />
            {announcement?.tenancy.id}<br />
            {announcement?.tenancy.name}<br />
            {announcement?.tenancy.email}<br />
            <div className="row"><br /></div>
            <div className="row">
                <div className="col-6 text-start">
                    {
                        announcement?.show
                            ? <button onClick={hideAnnouncement} className="btn btn-sm btn-warning">비공개</button>
                            : <button onClick={showAnnouncement} className="btn btn-sm btn-primary">공개</button>
                    }
                </div>
                <div className="col-6 text-end">
                    {
                        //hasRole("Manager") 
                        
                        authority && authority.hasAuthority(workspace + appName, "Manager")
                        ? announcement?.show
                            ? <button onClick={hideAnnouncement} className="btn btn-sm btn-warning">비공개</button>
                            : <button onClick={showAnnouncement} className="btn btn-sm btn-primary">공개</button>
                        : <></>
                    }
                    <Link to={`${workspace}${appName}`} className="btn btn-sm btn-secondary">목록</Link>
                </div>
            </div> */}

        {data && (<>
            <p>Detail</p>
            {data.title}<br/>
            {data.content}<br/>
            {data.tenancy.id}<br/>
            {data.tenancy.name}<br/>
            {data.tenancy.email}<br/>
            <div className="row"><br/></div>
            <div className="row">
                <div className="col-6 text-start">
                    {
                  // data?.show ? <button> 비공개 </button> : <button> 공개 </button>
                        data?.show
                            ? <button onClick={() => announcementMutation.mutate({
                                command: 'hide'
                            })}
                                      className="btn btn-sm btn-warning">비공개</button>
                            : <button onClick={() => announcementMutation.mutate({
                                command: 'show'
                            })}
                                      className="btn btn-sm btn-primary">공개</button>
                    }
                </div>
                <div className="col-6 text-end">
                    {
                        //hasRole("Manager")
                  		// authority?.hasAuthority(workspace+appName, "Manager") ? () : <></>
                        authority?.hasAuthority(workspace + appName, "Manager")
                            ? (<>{data.show		// data.show ? <button>비공개</button> : <button>공개</button>
                            ? <button onClick={() => announcementMutation.mutate({
                                command: 'hide'
                            })}
                                      className="btn btn-sm btn-warning">비공개</button>
                            : <button onClick={() => announcementMutation.mutate({
                                command: 'show'
                            })}
                                      className="btn btn-sm btn-primary">공개</button>
                            }</>) : <></>
                    }
                    <Link to={`${workspace}${appName}`} className="btn btn-sm btn-secondary">목록</Link>
                </div>
            </div>
        </>)}
    </Layout>
}

export default Detail;

0개의 댓글