1. 수동으로 새로고침한 경우에만 특정 함수를 trigger 시키는 hook(url Params 삭제)

 

useDeleteUrlParamsWhenManualRefresh.ts

  • 코드
// input 삭제할 url parms key list

import { useEffect } from "react";
import { useSearchParams } from "react-router-dom";

export default function useDeleteUrlParamsWhenManualRefresh(paramsToDelete) {
  const [searchParams, setSearchParams] = useSearchParams();

  const setFirstLoadFlag = () => {
    localStorage.removeItem("firstLoadDone");
  };

  const deleteUrlParamsWhenManuallyRefreshed = () => {
    paramsToDelete.map((param) => {
      searchParams.delete(param);
      searchParams.delete(param);
    });
    setSearchParams(searchParams);
  };

  useEffect(() => {
    window.addEventListener("beforeunload", setFirstLoadFlag);
    if (localStorage.getItem("firstLoadDone") === null) {
      localStorage.setItem("firstLoadDone", "1");
      deleteUrlParamsWhenManuallyRefreshed();
    }

    return () => {
      window.removeEventListener("beforeunload", setFirstLoadFlag);
    };
  }, []);
}

 

 

MyComponent.tsx

  • 사용하는 컴포넌트 코드
// 수동 새로고침시 필터 세팅 url params 삭제
useDeleteUrlParamsWhenManualRefresh(["detailType", "detailText"]);

 

 

  • 화면

1. Form 의 required field 확인하는 hook

  • input
    • fromRef: 타겟 Form element
    • formDataState : 관리되는 form 데이터 state
    • submitFlag : 제출 버튼 눌렀는지 여부
  • output
    • isFormValidated : validation 통과 되었는지 boolean

 

useFormValidation.ts

  • 코드
import { useEffect, useState } from "react";
// input : formRef, 제출할 formData, submitFlag
// output : 통과 여부

export const useFormValidation = ({ formRef, formDataState, submitFlag }) => {
  const [isFormValidated, setIsFormValidated] = useState(false);

  useEffect(() => {
    if (submitFlag) {
      const inputs = [...formRef.current.elements];
      console.log("inputs", inputs);
      inputs.forEach((el) => {
        // input & select 만 체크 && // required 만 체크
        if (["SELECT", "INPUT", "TEXTAREA"].includes(el.tagName)) {
          if (!el.value && el.required) {
            el.classList.add("is-invalid");
            if (el.parentNode.lastChild.id !== "required-noti-text") {
              const requiredErrorTextDiv = document.createElement("div");
              requiredErrorTextDiv.innerHTML = "Required";
              requiredErrorTextDiv.style.cssText = "color: #E63757; font-size: 10px; margin-top: 0.25rem";
              requiredErrorTextDiv.id = "required-noti-text";
              el.parentNode.appendChild(requiredErrorTextDiv);
            }
          } else {
            el.classList.remove("is-invalid");
            const requiredErrorTextDiv = el.parentNode.querySelector("#required-noti-text");
            if (requiredErrorTextDiv) el.parentNode.removeChild(requiredErrorTextDiv);
          }
        }
      });
      // 성공 여부
      const invalid = document.querySelectorAll(".is-invalid");
      if (invalid.length === 0) setIsFormValidated(true);
    } else {
      // submitFlag false 일때 valid 결과 false 리턴
      setIsFormValidated(false);
    }
  }, [submitFlag, formDataState]);

  return { isFormValidated };
};

 

 

MyComponent.tsx

  • 사용하는 컴포넌트 코드
const [submitFlag, setSubmitFlag] = useState(false);
const formElement = useRef<any>(null);
const [resultData, setResultData] = useState({});

const { isFormValidated } = useFormValidation({
  formRef: formElement,
  formDataState: bannerDetailPutData,
  submitFlag,
});

const onClickSave = () => {
	setSubmitFlag(true);
	if (isFormValidated) {
	  productListPostMutate.mutate(resultData);
	}
}

return (
	<Form ref={formElement}>
		<Button type="button" onClick={onClickSave}>save</Button>
		...
		<Row className="mb-2">
	    <Col md={12} xs={12}>
	      <Form.Control
	        required
	        as="textarea"
	        rows={1}
	        style={{ resize: "none" }}
	        placeholder="Input Meta Title"
	        disabled={pageInfo === "detail" && !editFlag}
	        name="metaTitle"
	        value={el?.metaTitle || ""}
	        onChange={(e) => onChangeDragItem(e, idx)}
	      />
	    </Col>
	  </Row>
		...
	<Form>
)

1. Resize 될 때마다 현재 window 의 width가 breakpoint 보다 up 인지 down 인지 물어보는 hook

  • input : 없음
  • output
    • breakpoints : up / down 메소드를 가지고 있는 객체

 

useResizeBreakpoints.ts

  • 코드
import { useState, useEffect } from "react";

interface GridBreakpoints {
  xs: number;
  sm: number;
  md: number;
  lg: number;
  xl: number;
  xxl: number;
}

interface Breakpoints {
  up: (bp: keyof GridBreakpoints) => boolean;
  down: (bp: keyof GridBreakpoints) => boolean;
}

interface UseResizeBreakpointsResult {
  breakpoints: Breakpoints;
}

export const useResizeBreakpoints = (): UseResizeBreakpointsResult => {
  const gridBreakpoints: GridBreakpoints = {
    xs: 0,
    sm: 576,
    md: 768,
    lg: 992,
    xl: 1200,
    xxl: 1540,
  };

  const [width, setWidth] = useState<number>(window.innerWidth);
  const [height, setHeight] = useState<number>(window.innerHeight);

  const [breakpoints, setBreakpoints] = useState<Breakpoints>({
    up: (bp) => width >= gridBreakpoints[bp],
    down: (bp) => width < gridBreakpoints[bp],
  });

  const updateDimensions = () => {
    setWidth(window.innerWidth);
    setHeight(window.innerHeight);
  };

  useEffect(() => {
    window.addEventListener("resize", updateDimensions);
    return () => window.removeEventListener("resize", updateDimensions);
  }, []);

  useEffect(() => {
    setBreakpoints({
      up: (bp) => width >= gridBreakpoints[bp],
      down: (bp) => width < gridBreakpoints[bp],
    });
  }, [width]);

  return { breakpoints };
};

 

 

MyComponent.tsx

  • 사용하는 컴포넌트 코드
const { breakpoints } = useResizeBreakpoints();

...

return (
	...
	{breakpoints.down("sm") ? <div>SM 사이즈보다 작을때 </div> : <div>SM 사이즈보다 클때 </div>}
	...
)

 

  • 화면
    • window 사이즈가 breakPoint 에서 sm 사이즈 보다 클 때

  • window 사이즈가 breakPoint 에서 sm 사이즈 보다 클 때

1. 타겟 element 가 viewport 안에 보이는지 판단하는 hook

  • input
    • element: 관찰하고자 하는 타겟 element React.ref
    • rootMargin : 관찰 타겟 주위의 여백, new IntersectionObserver() 생성자에, option 으로 들어가는 값.
      • 예시. "10px 20px 30px 40px" (위, 오른쪽, 아래, 왼쪽). 값은 백분율이 될 수 있습니다. 기본값은 0.
      • 타겟 주변의 여백도 포함해서, 그 여백까지 타겟으로 보고, viewport 에서 사라졌는지 판단함.
      • IntersectionObserver.option.rootMargin 더보기
  • output
    • isVisible : 해당 타겟이 현재 viewport 안에 보이는지 여부 boolean.
    • observer : 생성한 IntersectionObserver 인스턴스
      • unmount 시키면서, 생성한 observer 인스턴스 관찰 unobserve 시키기 위함

 

useVisibilityObserver.ts

  • 코드
import { useState, useEffect, RefObject } from "react";

interface VisibilityObserverResult {
  isVisible: boolean;
  observer: IntersectionObserver | null;
}

const useVisibilityObserver = (
  element: RefObject<HTMLElement | null>,
  rootMargin: string,
): VisibilityObserverResult => {
  const [isVisible, setState] = useState(false);
  const [observer, setObserver] = useState<IntersectionObserver | null>(null);

  useEffect(() => {
    if (element.current) {
      const intersectionObserver = new IntersectionObserver(
        ([entry]) => {
          setState(entry.isIntersecting);
        },
        { rootMargin },
      );

      setObserver(intersectionObserver);

      intersectionObserver.observe(element.current);

      // Cleanup: Disconnect the observer when the component unmounts
      return () => {
        intersectionObserver.disconnect();
      };
    }
  }, [element, rootMargin]); // Make sure to include 'element' and 'rootMargin' in the dependency array

  return { isVisible, observer };
};

export default useVisibilityObserver;

 

 

MyComponent.tsx

  • 사용하는 컴포넌트 코드
const targetElRef = useRef<HTMLDivElement | null>(null);
const { isVisible: inViewport, observer } = useVisibilityObserver(targetElRef, "100px");

useEffect(() => {
  return () => {
    observer && targetElRef.current && observer.unobserve(targetElRef.current);
  };
}, [observer]);


...

return (
	...
	<div ref={targetElRef}>TEST</div>
	
	...
	
	<div style={{ margin: "10px", display: inViewport ? "block" : "none" }}>BOTTOM</div>
	...
)

 

  • 화면
    • TEST 박스가 화면안에 보일때는 BOTTOM 박스도 노출됨

  • TEST 박스가 화면 밖으로 사라지면 BOTTOM 글씨도 사라짐

1. async 로 Mock Data 제공하는 hook

  • input
    • resolvedData: 제공할 mock data
    • waitingTime : setTimeout 기다리는 시간, 몇 초 후 리턴
  • output
    • { loading, setLoading, data, setData } : 로딩, 리턴받는 데이터 state

 

useFakeFetch.ts

  • 코드
import { useEffect, useState } from "react";

interface FakeFetchResult<T> {
  loading: boolean;
  setLoading: React.Dispatch<React.SetStateAction<boolean>>;
  data: T[];
  setData: React.Dispatch<React.SetStateAction<T[]>>;
}

const useFakeFetch = <T>(resolvedData: T[], waitingTime = 500): FakeFetchResult<T> => {
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState<T[]>([]);

  useEffect(() => {
    let isMounted = true;

    const timeoutId = setTimeout(() => {
      if (isMounted) {
        setData(resolvedData);
        setLoading(false);
      }
    }, waitingTime);

    return () => {
      isMounted = false;
      clearTimeout(timeoutId);
    };
  }, [resolvedData, waitingTime]);

  return { loading, setLoading, data, setData };
};

export default useFakeFetch;

 

MyComponent.tsx

  • 사용하는 컴포넌트 코드
const { loading: priceLoading, data: pricing } = useFakeFetch(
  pricingData,
  1000
);
const { data: newNotifications, setData: setNewNotifications } =
  useFakeFetch(rawNewNotifications);
const { data: earlierNotifications, setData: setEarlierNotifications } =
  useFakeFetch(rawEarlierNotifications);

 

useQuery 를 사용하면서 특정 조건에 따라서 query 를 실행/미실행 하려고 한다

  const { data, error, isLoading } = useQuery(
    ["test", "detail", id],
    () => getDetail({ id }),
	{enabled: !!id}
   )

 

 

id가 있을 때만 query 요청하기

조건 기준이 되는 값은 반드시 queryKey 에 포함이 되어있어야 enabled 가 작동된다

 

 

id 가 없어서 query 요청을 안하는 경우인데도 isLoading = true 로 설정되어, 로딩 스피너가 계속 돌았다.

 

이에 대한 해결책은

react-query v4에서 isInitialLoading이라는 새로운 인자를 반환해준다.

 

react-query 공식 문서에서 isInitialLoading 에 대한 설명을 보면, isFetching 과 isLoading 의 AND 연산 값이다

즉 실제로 fetching 하고 있는가 + loading 중인가 체크함

  • isInitialLoading: boolean
    • Is true whenever the first fetch for a query is in-flight
    • Is the same as isFetching && isLoading

 

 

관련 링크

https://github.com/TanStack/query/issues/3584

 

[Beta] Disabled query has `isLoading: true` property · Issue #3584 · TanStack/query

Describe the bug In react-query@beta version has strange behaviour. By default query that has enabled: false option has isLoading: true property (see code sandbox for details). In stable release re...

github.com

 

https://tanstack.com/query/v4/docs/react/reference/useQuery

 

useQuery | TanStack Query Docs

Does this replace [Redux, MobX, etc]?

tanstack.com

 

  // 최초 마운트시 실행 되지 않고, dependencies 가 실제 변경된 경우에만 실행
  
  const didMount = useRef(false);
  useEffect(() => {
    if (didMount.current) {
      setValue("connectProductId", null);
    } else didMount.current = true;
  }, [watch("countryId")]);
목적 : 테이블 데이터를 엑셀로 Export

 

방법1) 프론트에서 처리 -> UI 상 보여지는 테이블 데이터를 그대로 엑셀 추출
방법2) 백엔드에서 처리 -> 프론트에서 가공되지 않은 원본 데이터를 추출

 

1. 프론트 처리

엑셀 처리를 위한 라이브러리 사용

https://www.npmjs.com/package/xlsx

 

xlsx

SheetJS Spreadsheet data parser and writer. Latest version: 0.18.5, last published: 2 years ago. Start using xlsx in your project by running `npm i xlsx`. There are 4148 other projects in the npm registry using xlsx.

www.npmjs.com

import * as XLSX from "xlsx";

 

ExcelButton 컴포넌트의 onClick 함수

const worksheet = XLSX.utils.json_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
XLSX.writeFile(workbook, "OrderList.xlsx");

 

 

2. 백엔드에서 처리하여 excel 파일을 blob 형태로 리턴 -> excel 파일을 링크화하여 로컬에 다운

컴포넌트 호출 부분

import { getOrderListExcelFile } from "hooks/queries/order";


<ExportButton data={orderListData} excelExportFunction={getOrderListExcelFile} className="p-1 me-3 fs--1" />

 

 

API 요청 함수 부분

get 요청의 option에서 responseType : "blob" 로 지정해주어야 함

이 요청 함수를 <ExportButton> 컴포넌트의 excelExportFunction Prop으로 넘겨준다

// order list excel download
export const getOrderListExcelFile = async () => {
  return axiosInstance
    .get(`/order/download`, { responseType: "blob" })
    .then((res) => res.data)
    .catch((error) => error);
};

 

* 추가 Post 로 요청하기

// order list excel download
export const getOrderListExcelFile = async () => {
  return axiosInstance
    .post(`/order/download`, null, { responseType: "blob" })
    .then((res) => res.data)
    .catch((error) => error);
};

 

 

<ExportButton> 컴포넌트 

방법1(프론트), 방법2(백) 동시 커버

API로 리턴받은 데이터와 원하는 추출 파일 형식(Excel) 을 'downloadFile' 함수로 전달

import React from "react";
import { Button } from "react-bootstrap";
import * as XLSX from "xlsx";
import classNames from "classnames";
import { toast } from "react-toastify";
import { downloadFile } from "helpers/download-file";

interface IExportButtonProps {
  data: any;
  excelExportFunction?: () => Promise<any>;
  className: string;
}

const CONTENT_TYPE_EXCEL = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

export default function ExportButton({ data, excelExportFunction, className }: IExportButtonProps) {
  const onClickExcel = async () => {
    // 부모 컴포넌트에서 별도의 다운로드 API 함수(excelExportFunction)를 prop으로 내려보내줬을 경우
    if (excelExportFunction) {
      const res = await excelExportFunction();
      // 에러 발생시 res.response.data에 담긴 blob을 parse해서 error message 출력
      if (res.response && res.response.status !== 200) {
        const blobToText = await res.response.data.text();
        const errorResponse = JSON.parse(blobToText);
        toast.error(errorResponse.message);
        return;
      }
      downloadFile(res, CONTENT_TYPE_EXCEL);
    } else {
      // 부모 컴포넌트에서 다운로드 API를 내려보내지 않은 경우 프론트에서 처리
      const worksheet = XLSX.utils.json_to_sheet(data);
      const workbook = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
      XLSX.writeFile(workbook, "OrderList.xlsx");
    }
  };
  return (
    <Button variant="falcon-default" className={classNames(className)} onClick={onClickExcel}>
      Export
    </Button>
  );
}

 

 

blob 말아서 다운로드 링크로 만들고 로컬에 다운로드 하는 함수

리턴 받은 res 데이터(blob), 추출할 컨텐트 타입(Excel 파일 형태) 를 매개변수로 전달받고

<a>링크를 만들고, body 에 추가한 후 click하여 다운로드 후 삭제 처리

import dayjs from "dayjs";
const currentDate = dayjs(new Date()).format("YYYY-MM-DD");

export const downloadFile = (file, contentType) => {
  const a = document.createElement("a");
  const blob = new Blob([file], { type: contentType });
  a.href = window.URL.createObjectURL(blob);
  a.target = "_blank";
  a.download = `downloaded_file_${currentDate}`;
  a.style.display = "none";
  document.body.appendChild(a);
  a.click();
  URL.revokeObjectURL(a.href);
  document.body.removeChild(a);
};

 

목표 : React 에서 무한 스크롤 구현하기 (Infinity Scroll in React)

여정 : scroll Event Listener -> Throttle 로 최적화 -> Intersection Observer API 사용

 

1. 단순 구현

  • 스크롤 이벤트를 참조하여 다음 데이터 API 호출

코드 구현

  • 가정
    • fetchData() 는 API 를 요청하는 함수
    • 예제에서는 console.log('fetch API 요청') 찍히는 걸로 API 요청을 대신함
useEffect(() => {
	const handleScroll = () => {
		const { scrollTop, offsetHeight } = document.documentElement;
		if (window.innerHeight + scrollTop >= offsetHeight) {
			// 스크롤 위치 바닥일때 데이터 페치
			fetchData() // -> API 요청하는 함수
			console.log('fetch API 요청');
		}
	};
	// 화면 진입시 데이터 페치
	fetchData() // -> API 요청하는 함수
	console.log('fetch API 요청');
	window.addEventListener('scroll', handleScroll);
	return () => window.removeEventListener('scroll', handleScroll);
}, []);
  • 개선 포인트
    • documentElement.scrollTop과 documentElement.offsetHeight는 리플로우(Reflow)가 발생하는 참조이므로 API를 과다로 요청하는 경우 발생 가능

 

2. 쓰로틀 (Throttle) 적용

  • 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
  • 동일 이벤트가 반복적으로 시행되는 경우 이벤트의 실제 반복 주기와 상관없이 임의로 설정한 일정 시간 간격(밀리세컨드)으로 콜백 함수의 실행을 보장

코드 구현

  1. 기본 Throttle 함수(ver1)
function throttle(callback, delay = 300) {
	let waiting = false

	return (...args) => {
		if (waiting) return

		callback(...args)
		waiting = true
		setTimeout(() => {
			waiting = false
		}, delay)
	}
}
  • callback: 실행 대상이 되는 콜백 함수
  • delay : 얼마 간격으로 함수를 실행할 지 결정하며 millisecond 단위, 기본값으로 0.3초를 주었음
  • waiting 상태가 true 인 경우 실행 없음
  • 최초 함수가 호출되었을 때 waiting은 false
  • 이 때 콜백 함수를 실행한 뒤 다시 waiting은 true가 됩니다.
  • waiting이 true가 되었을 때 delay 밀리초 후에는 waiting이 강제로 false가 되고, 다시 콜백 함수가 실행이 됩니다.

1-2. 기본 Throttle 함수(ver2)

function throttle(callback, delay = 3000) {
	let timer;
	return (...args) => {
		if (timer) return;
		timer = setTimeout(() => {
			callback(...args);
			timer = null;
		}, delay);
	};
}
  • timer 를 생성하는 역할
  • 생성해둔 timer 가 있으면, 콜백함수 실행 없이 return
  • timer 가 없으면, timer 를 만듦
  • 만든 timer 는 delay 시간 이후에 콜백 함수를 실행하는 타이머

⇒ 두가지 throttle 함수를 각각 적용해봤을 때 동작에 차이가 발생했음

 

2. handleScroll 함수에 throttle 함수를 적용

useEffect(() => {
	const handleScroll = throttle(() => {
		const { scrollTop, offsetHeight } = document.documentElement;
		if (window.innerHeight + scrollTop >= offsetHeight) {
			// 스크롤 위치 바닥일때 데이터 페치
			console.log('fetch API 요청');
		}
	});
	// 화면 진입시 데이터 페치
	console.log('fetch API 요청');
	window.addEventListener('scroll', handleScroll);
	return () => window.removeEventListener('scroll', handleScroll);
}, []);
  • ver2 throttle 함수의 경우 : 정상 동작
    1. 스크롤 바닥
    2. 3000ms 대기
    3. timer (setTimeout) 생성
    4. callback 함수 실행
    5. timer 해제
    6. 스크롤 위로 갔다가 다시 바닥
    7. 3000ms 대기
  • ver1 throttle 함수의 경우 이상 동작
    • 함수의 코드 순서를 확인해 보자
      • throttle 함수 ver1
      function throttle(callback, delay = 3000) {
      	let wait = false;
      
      	return (...args) => {
      		if (wait) {
      			return;
      		}
      
      		callback(...args);
      		wait = true;
      		setTimeout(() => {
      			wait = false;
      		}, delay);
      	};
      }
      
      • 이상동작 원인
        • 일단 스크롤 이벤트가 생길 때 마다 callback 함수가 실행되는데,
        • 스크롤 이벤트가 트리거 되는 시작점의 스크롤 위치 값을 들고 감
        • if (window.innerHeight + scrollTop >= offsetHeight) {
        • 그래서 스크롤 바닥 조건에 걸리지 않음
        • fetch 함수를 실행하지 않음
  • 화면 예시
    • 한번에 끝까지 스크롤 한 상태
    • callback 함수가 호출됨

  • scrollTop 위치를 “3” 으로 물고 있음
  • fetch 할 조건 (scrollTop이 아래에 위치해 있는가?) 에 해당되지 않아서 fetch 함수가 실행되지 않는다
  • delay 시간 이후에 callback 함수를 실행해야 원하는 scrollTop 값으로 조건체크를 할 수 있음

  • 함수 변형
function throttle(callback, delay = 3000) {
		let wait = false;

		return (...args) => {
			if (wait) {
				return;
			}
			wait = true;
			setTimeout(() => {
				callback(...args);
				wait = false;
			}, delay);
		};
	}

  • delay 시간 이후 callback 함수가 호출 되어서 최신 scrollTop 위치값을 가져올 수 있

 

⇒ 스크롤 이벤트 특성상 delay 이후에 콜백함수가 실행되는 것이 정상 동작이었으나, 케이스에 따라 콜백을 먼저 실행하고, delay 가 있어야 하는 경우도 있을 것

 

3. Intersection Observer API (체신 문법 적용^^)

  • Intersection Observer API 적용
  • 교차 관찰자 API
  • 브라우저에서 제공하는 API
  • 크롬 51버전부터 사용가능
  • 위에서 확인했듯이, 스크롤 이벤트는 일단 스크롤이 발생하면 트리거됨
  • 우리가 원하는 것은 스크롤이 아래에 도달했을 때만 트리거 시키고 싶음

코드 구현

  • new IntersectionObserver() 생성자 표현식을 사용하여, observer 객체를 생성하여 사용
  1. 생성
    • 교차될때 실행할 콜백함수를 제공해야 한다 → fetchData 함수
    • options 로 root / rootMargin / threshold 값을 커스텀 할 수 있음
  2. 설정
    • options 값 커스텀
      • root : 대상이 되는 객체의 가시성을 확인 할 때 사용하는 뷰포트 요소, 기본값은 브라우저 뷰포트

      • rootMargin : root 가 가진 여백, 교차성을 개산하기 전에 적용됨

      • threshold : observer의 콜백함수가 실행될 대상 요소의 가시성 퍼센테이지, 10%만 보여도 수행하는가?, 100% 다보였을때 실행하는가

 

3. 관찰대상 지정하기

  • 요소를 observer.observe 메소드에 등록해주면 된다
let target = document.querySelector('#listItem'); 
observer.observe(target);

4. 콜백함수 구현하기

  • 콜백함수 기본 args
let callback = (entries, observer) => {
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};
  • IntersectonObserverEntry 리스트를 첫번째 인자로 받고, observer 자체를 두번째 인자로 받는다
  • React 용 useIntersect hook
const { useRef, useCallback, useEffect } = require('react');

const useIntersect = (onIntersectFn, options) => {
	const ref = useRef(null); // 1
	const callback = useCallback(
		(entries, observer) => {
			entries.forEach((entry) => {
				if (entry.isIntersecting) onIntersectFn(entry, observer);
			});
		},
		[onIntersectFn]
	);

	useEffect(() => {
		if (!ref.current) return;
		const observer = new IntersectionObserver(callback, options); // 2
		observer.observe(ref.current); // 3
		return () => observer.disconnect();
	}, [ref, options, callback]);

	return ref;
};

export default useIntersect;
  1. 관찰 타겟 ref 생성
  2. observer 생성
  3. observer.observe 에 관찰 타겟 ref 연결
  4. ref unmount 시 observer disconnect 시키기 (삭제)
  5. callback 함수 선언
    • entry 객체 내부 key

    • entry 가 isIntersecting true 인터섹트 트리거 되면,
    • onIntersectFn 함수를 실행
    • onIntersectFn은 useIntersect 훅 선언시 전달 → fetchData 함수
  • useIntersect 훅 사용부분
    • 핵심 내용은 fetchData 함수를 실행해야함
    • 타겟에 대한 관찰을 멈출지는 상황에 따라 판단
const ref = useIntersect(async (entry, observer) => {
	// observer.unobserve(entry.target);
	// if(hasNextPage && !isFetching) {
	// 	console.log('fetchNextPage');
	// }
	console.log('fetchNextPage');
});

 

4. 폴리필

  • safari 에서 지원하고 있지 않음 폴리필 필요

https://github.com/GoogleChromeLabs/intersection-observer

https://github.com/w3c/IntersectionObserver

$ npm i intersection-observer
import 'intersection-observer'

const io = new IntersectionObserver(콜백함수, 옵션)

const 관찰대상 = document.querySelectorAll(관찰대상)

// IE에서 사용하기
Array.prototype.slice.call(관찰대상).forEach(elim => {
  io.observe(elim)
})

 

참고

 

 

  • 프론트 : localhost:3000 띄워서 로컬에서 개발
  • API서버(백) : http://~.com dev서버를 올려서 해당 dev 서버에 요청

 

  • 브라우저에서 보내는 요청 → CORS 발생
    • 브라우저는 localhost:3000을 달고 있는데, 다른 주소(API서버)로 부터 온 응답값을 허용하지 않음 (동일출처가 아니기 때문)
  • 해결책 : 브라우저에서 나가는 요청을 API 서버 주소로 둔갑시킨다
    • proxy 설정

 

  • http-proxy-middleware 패키지 설치
  • src/setupProxy.js
    • api/ 로 시작하는 요청 url 인 경우 설정한 target 주소에서 요청하는 것으로 둔갑해줌
    • /api 부분은 빠져야 하므로 pathRewrite 도 시켜준다
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = (app) => {
    app.use(createProxyMiddleware('/api', {
        target: 'http://myservices.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
    })
    );
}
  • api 요청 부
import axios from 'axios';

export const postLogin = ({ id, password }) => {
	return axios.post('/api/auth/access_token', {
		id,
		password,
	});
};

https://12716.tistory.com/entry/ReactJS-CORS이슈-해결을-위한-Proxy-설정

 

[ReactJS] CORS이슈 해결을 위한 Proxy 설정

앞선 포스팅에서 프론트서버의 3000번포트에서 백엔드서버인 5000번포트로 요청을 보낼때 CORS policy에 의해서 막혔다라는 에러가 발생했었는데요. 서버는 포트가 5000번이고 클라이언트는 3000번으

12716.tistory.com

 

 

운영에 프론트를 배포하고 나서 동일한 코드가 어떻게 동작하는 지 확인

일단 운영에서는 프록시 설정이 작동하지 않는다

http-proxy-middleware 패키지는 자동으로 개발환경에서만 작동하도록 세팅되어있다

 

운영환경에서는 그럼 CORS 에러가 발생하지 않는가?

 

기본적으로 알고 있어야할 개념이 있다

서브 도메인(Sub Domain)

서브 도메인은 보조 도메인으로써, URL로 전송하거나 계정 내의 IP 주소나 디렉토리로 포워딩되는 도메인 이름의 확장자이다.

예를들어 네이버는 여러 서비스들을 아래와 같은 서브도메인을 통해 사용자가 접근할 수 있도록 한다.

  • 네이버 블로그 : blog.naver.com
  • 네이버 메일 : mail.naver.com
  • 네이버 금융 : finance.naver.com
  • https://app.jakearchibald.com
  • https://other-app.jakearchibald.com

프론트 주소가 myapp.com 

API 서버 주소가 api-myapp.com

 

위와 같으면 CORS 발생하지 않음

 

당연히 이런 개념을 알고, 주소를 팠겠거니,,, 한다면,

운영에서는 

 

 

Proxy 설정

  • 로컬 개발에서 설정
    1. 프록시 (/api) 로 설정
      • login.js : axios 요청부
      import axios from 'axios';
      
      export const postLogin = ({ id, password }) => {
      	return axios.post('/api/auth/access_token', {
      		id,
      		password,
      	});
      };
      
      • setupProxy.js
        • 프록시 설정에서 target 값을 주면 axios 에서 baseURL 설정을 안해도 됨
      const { createProxyMiddleware } = require('http-proxy-middleware');
      
      module.exports = function (app) {
      	app.use(
      		'/api',
      		createProxyMiddleware({
      			target: 'http://myapp-api.myapp.com/>',
      			changeOrigin: true,
      			pathRewrite: { '^/api': '' },
      		})
      	);
      };
      • 결과
        • 서버로 요청이 정상적으로 갔고, 서버로부터 응답을 받음
        • Request URL : localhost:3000/api/auth/access_token

 => 로컬) 요청 → localhost:3000 → 프록시 서버 → 프록시에 설정한 target 서버로 요청

 

 

 

 

2. 프록시('/api/~') 설정 + axios baseURL 변경

  • login.js
export const postLogin = ({ id, password }) => {
	return axiosInstance.post('/auth/access_token', {
		id,
		password,
	});
};
  • axiosInstanse.js
const axiosInstance = axios.create({
	baseURL: 'http://myapp-api.myapp.com',
	headers: {
		'Content-Type': 'application/json',
	},
});
  • setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
	app.use(
		'/api',
		createProxyMiddleware({
			target: 'http://myapp-api.myapp.com/>',
			changeOrigin: true,
			pathRewrite: { '^/api': '' },
		})
	);
};
  • 결과
    • 서버로부터 정상(여기서는 401이 정상임, 잘못된 계정을 입력했기 때문에 unauthorized, 내가 받고싶었던 응답) 응답을 받음

  • 의문점 : 근데 /api~로 시작을 안했는데.. CORS 가 어떻게 통과 되었는가?
  • 로컬에서 프록시 설정을 빼보자-> 잘됨... 백엔드에서 전체 받아주는거로 처리했나..?

 

 

백에서 처리를 해주었다면, 프론트에서 프록시 처리하지말고, 그냥 axios baseURL 만 바꺼서 보내면 된다

 

 

  •  

+ Recent posts