TIL 23-11-11

thisisyjin·2023년 11월 11일
0

TIL 📝

목록 보기
95/113

TIL 23-11-11

  1. Vite
  2. Tailwind CSS & PostCSS
  3. Project (Poke wiki)

Vite / PostCSS

Vite

$ npm init vite
# 별도로 패키지 설치 해줘야 함
$ npm install

Tailwind CSS

$ npx tailwindcss init
  • tailwind.config.cjs
  • postcss.config.cjs

위와 같은 설정 파일을 작성해줌.

index.css 내부 스타일링 초기화 후 아래와 같이 작성.

@tailwind base;
@tailwind components;
@tailwind utilities;

PostCSS

  • Javascript를 CSS로 변환하는 도구

  • preset-env-cssdb, autoprefixer, stylelinter 등 다양한 플러그인 사용 가능하게 해주는 도구

  • 코드 가독성 향상 (접두사)

  • 최신 CSS (폴리필 결정)

  • CSS 모듈 (클래스명 중복 X)

  • CSS 에러 방지 (console에 에러 나타남)

autoprefixer

  • caniuse의 값을 이용하여 접두사 등을 적용해줌

Project

Axios

  • 서드파티 모듈. 설치 필요.
  • 브라우저, Node.js 를 위한 Promise 기반의 HTTP 비동기 통신 라이브러리.
$ npm install axios

React useEffect

  1. 컴포넌트 마운트
  2. state 초기화
  3. return
  4. useEffect (init)
  5. dependencies state 변화
  6. useEffect (dependencies 빈 배열인 경우 X)

Response.dara 포맷

  • API의 response.data를 다 가져오기에는 불필요한 필드가 많은 경우
  • formatData 함수를 이용하면 좋다.
const DataCard = ( name, url ) => {
   const [data, setData] = useState([]);

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

  const fetchData = async () => {
  try {
        const response = await axios.get(url);
        const formatedData = formatData(response.data);
        setData(formatedData);
    } catch (error) {
        console.error(error);
    }
  }

  const formatData = (params) => {
    const { id, name, category} = params;
    const data = {
       id,
       name,
       category: category[0].name
    }
  }
}

Optional Chaining

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Optional_chaining

const type = data?.type;
// data가 있는 경우에 data.type을 가져옴
// data가 없는 경우에는 undefined 에러가 뜸

Image Lazy Loading

  • 페이지 내 이미지를 실제로 보여주는게 아닌 화면에 보일 필요가 있을 때 불러오는 테크닉
  • 스크롤이 길고, 이미지가 많은 경우에 활용
    (초기 로딩 속도 단축 가능)
  • onLoad 이벤트 핸들러를 이용해서 로드가 되고 난 후도 정의 가능
<img
         src={src}
         loading="lazy"
         onLoad={() => setLoads(false)}
/>
const LazyImage = ({ url, alt }) => {
  const [loads, setLoads] = useState(true);
  const [opacity, setOpacity] = useState(0);
  
  useEffect(() => {
    loads ? setOpacity(0) : setOpacity(100);
  }, [loads]);

  return (
    <>
      {
          loads && <LoadingBar />
       }
       <img 
                   src={url}
                   alt={alt}
                   loading="lazy"
                   onLoad={() => setLoads(false)}
          />
    </>
  )

더보기 기능

  • 필요 State
  1. offset (시작 값)
  2. limit (몇 개의 데이터를 가져올지)

예> 한 번에 20개씩 불러올 때
offset: 0 -> 20 -> 40
limit: 20

첫 번째 fetch와 그 이후 fetch 구분

  • fetchData 함수에 params로 true, falsefh wjsekf
const [offset, setOffset] = useState(0);
const [limit, setLimit] = useState(20);

  
useEffect(() => {
  fetchData(true); // 처음에 가져올 때에는 true 전달
  
}, []);

const fetchData = async(isFirstFetch) => {
  try {
    const offsetValue = isFirstFetch ? 0 : offset + limit;
    const url = `~~~url/?limits=20&offset=${offsetValue}`;
  	const response = awiat axios.get(url);
    setDatas([...datas, ...response.data]);
    setOffset(offsetValue);
  } catch (error) {
    console.error(error);
  }
}
  
return (
  <>
    
    <button
      onClick={() => fetchDate(false)}
      >더 보기</button>
  </>
)

검색 기능

  • state 필요
  1. 검색 키워드 (searchTerm)
const [searhTerm, setSearchTerm] = useState('');

...

const handleSearchInput = async(e) => {
  // onChange 때마다 get 요청 보냄
  setSearchTerm(e.target.value);
  if (e.target.value.length > 0) {
    try {
      const response = await axios.get(`~~url/${e.target.value}`);
      const data = {
        url: `~~url/${response.data.id}`,
        name: searchTerm
      }
      setDatas([data]);
    } catch (error) {
      setDatas([]);
      console.error(error);
    }
  } else {
    // 검색 값이 ''인 경우, 모든 데이터를 다 보여줌
    fetchData(true); // 초기 값 불러옴
  }
}


return (
  <>
    <form
    >
      <input 
        type="text"
        onChange={handleSearchInput}
        value={searchTerm}
      />
      <button type="submit">검색</button>
    <form/>
  </>
)

Custom Hooks

useDebounce

  • onChange시마다 GET 요청을 보내기엔 부하가 걸리기 때문에
  • 일정 딜레이를 준 후에 요청 보내도록 -> setTimeout

/src/hooks 폴더 생성 후 useDebounce.js 파일 생성

export const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);  // 1초 지나기 전에 변경되면 clearTimeout을 통해 새롭게 1초를 기다림
    }
  }, [value, delay]);
  
  return debouncedValue;
}
  • hooks 사용
import { useDebounce } from '../hooks/useDebounce';

const [searchTerm, setSearchTerm] = useState('');

const debouncedSearchTerm = useDebounce(searchTerm, 500);

useEffect(() => {
  fetchData(debouncedSearchTerm);
}, [debouncedSearchTerm]);

const fetchData = async(searchTerm) => {
   try {
     const response = await axios.get(`~~url/${searchTerm}`);
     // setDatas
   } catch (error) {
     // setDatas([])
     console.error(error);
   }
}

const onChangeInput = (e) => {
  setSearchTerm(e.target.value);
}

리팩토링

  • GET 요청 - 최초 1번만 (1000개 데이터 다 가져옴)

  • 1000개 데이터를 저장해서

  • 페이지네이션 or 더보기 실행할 때 마다 필터해서 보여줌

  • 1000개의 데이터를 가지고 있음

  • 그러나, 1000개의 데이터를 다 보여주지는 않음

  1. allDatas
  2. displayedDatas

두 가지 상태로 나눠서 관리하기.

  1. useEffect(최초) -> fetchData (1000개 데이터 가져와서 allDatas에 저장)
  2. filterDisplayedData -> allDatas.filter (0개 -> 20개 -> 40개 ...)
const [allDatas, setAllDatas] = useState([]);
const [displayedDatas, setDisplayedDatas] = useState([]);
const limitNum = 20; // 한 번에 보여주는 데이터 수
const url = `~~url/?limit=1000&offset=0`

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

const fetchData = async () => {
  try {
    const response = awiat axios.get(url);
    setAllDatas(response.data.results); // 전체 데이터 1000개 저장
    setDisplayedDatas(filterDisplayedData(response.data.results);
  } catch (error) {
    console.error(error);
  }
}

const filterDisplayedData = (allDatas, displayedDatas=[]) => {
  const limit = displayedDatas.length + limitNum;
  // displayedDatas가 0개 -> 20개 -> 40개 -> ...
  
  const array = allDatas.filter((_, index) => index + 1 <= limit); 
  // allDatas는 1000개 데이터가 있고, 여기서 0~19 까지만 필터링함
  return array;
}

return (
   ...
  <button onClick={() => setDisplayedDatas(filterDisplayedData(allDatas, displayedDatas))>더보기</button>
);
  
  • 처음: displayedDatas = []
  • filterDisplayedData 실행되면
    length가 0 -> 20 -> 40 ...
  • filterDisplayedData는 최초 20개 -> 더보기 클릭 시마다 20개씩 추가
  • 더보기 버튼을 더 볼 데이터가 존재할 경우에만 보이도록 조건문 설정
  • 더 볼 데이터가 있는 조건
  1. allDatas - displayedDatas > 0인 경우
    -> 즉, 전체 데이터가 1000개이고 displayedDatas가 1000개 미만
    -> allDatas > displayedDatas
  2. 검색 결과를 볼 경우에는 더보기 버튼이 없어야 함
    -> 즉, displayedDatas.length가 1일 때는 없어야 함
    -> displayedDatas.length !== 1 이여야 함
return (
   ...
  {(allDatas > displayedDatas) && (displayedDatas.length !== 1) && (<button onClick={() => setDisplayedDatas(filterDisplayedData(allDatas, displayedDatas))>더보기</button>)}
  
);

Auto Complete

  • allDatas
  • setDisplayedDatas
    를 props로 넘겨줌

검색 기능을 이전에는 GET 요청으로 했지만,
이제는 모든 데이터를 가지고 있기 때문에 해당 name 필드에 검색값을 포함하는 것을 가져오면 된다.

const AutoComplete = ({allDatas, setDisplayedDatas, }) => {
  const [searchTerm, setSearchTerm] = useState('');
  
  const handleSubmit = (e) => {
    // 폼 제출 시 (엔터 or 검색 버튼 클릭)
    e.preventDefault();
    
    let text = searchTerm.trim(); // 공백 제거
    setDisplayedData(filterNames(text));
    setSearchTerm('');
  }
  
  const filterNames = (Input) => {
    const value = Input.toLowerCase();
    retrun value 
    	? allDatas.filter(e => e?.name.includes(value))
    	: []
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={searchTerm} 
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <button>검색</button>
    </form>
  )
}

검색 시 하단에 자동완성 구현

  • input onChange 시
const checkEqualName = (input) => {
  const filteredArray = filterNames(input);
  // 완벽히 일치하는 이름이 있으면 자동완성에서 없애기 (빈 배열 반환)
  return filteredArray[0]?.name === input ? [] : filteredArray
}
  • UI 부분
  1. checkEqualName(input)이 빈 배열이면 (즉, 결과값과 일치하면) - 자동완성 박스 사라짐)
  2. checkEqualName이 빈 값이 아니면 (filter includes input) - 자동완성 박스 보여줌
    2-2. 자동완성 버튼 클릭하면 input을 해당 값으로 변경해줌
    onClick={() => setSearchTerm(e.name)}
return (
  ...
  { checkEqualName(searchTerm).length > 0 && (
     <ul>
     {checkEqualName(searchTerm).map((e, i) => (
       <li key={`button-${i}`} onClick={() => setSearchTerm(e.name)}>
         {e.name}
       </li>
     ))}
     </ul>
   )
)
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글