타입스크립트 사용이유❓


 

타입스크립트란 

TypeScript는 자바스크립트의 상위 집합(superset)으로, 자바스크립트 문법에 타입 체크를 위한 문법들이 추가된것

 

  1. 보다 신뢰할 수 있는 코드: 타입 체크를 통해 런타임 오류를 줄이고, 코드의 예측 가능성을 높인다.
  2. 규모 확장성: 코드베이스가 커질수록 타입 시스템 덕분에 안전하고 효율적으로 관리가 가능하다. (faster safely)
  3. 개발 도구 지원: 강력한 코드 자동 완성을 지원하여 생산성을 높인다.

 

✔️ JavaScript의 단점 상쇄

  • 실행 시간에 결정되는 변수 타입 -> 컴파일 시간에 변수의 타입을 체크
  • 약한 타입 체크 -> VS Code에 코드를 입력하는 순간 에러 메시지 발생
  • 너무나도 물렁물렁한 객체 -> 정의되지 않은 프로퍼티를 연산하여 NaN이 되는 현상을 미연에 방지

✔️ 향상되는 생산성

✔️ 높아지는 안전성

  • TypeScript 코드는 안정성과 가독성이 높아져 개발 및 유지 보수 과정에서 시간을 절약
  • 정적 언어에 익숙한 프로그래머들과의 협업도 더 빠르고 원활히 할 수 있음

 

 

 

1.Boolean

  • 2가지의 상태(켜짐/꺼짐, 유효함/유효하지 않음)를 표현하고 싶은 경우
  • boolean 타입은 참(true) 또는 거짓(false)
  • 조건문, 비교 연산 등에서 주로 사용

 

✏️ 사용예시

function isValidPassword(password: string): boolean {
  return password.length >= 8;
}

const password = "q1w2e3r4!";
const valid = isValidPassword(password);

if (valid) {
  console.log("유효한 패스워드입니다!");
} else {
  console.log("유효하지 않은 패스워드입니다!");
}

 

 

 

2.Number

  • TypeScript에서 사용하는 모든 숫자
  • 정수는 short, int, long
  • 실수는 float, double
  • 모든 수치 연산에 사용되는 값은 number 타입으로 명시

 

✏️ 사용예시

function calculateArea(radius: number): number {
  return Math.PI * radius * radius;
}

const radius = 5;
const area = calculateArea(radius);
console.log(`반지름이 ${radius}인 원의 넓이: ${area}`);

 

 

3.String

  • 텍스트 데이터
  • 텍스트와 텍스트가 합쳐져야 되는 경우
  • 텍스트에서 특정 문자열을 찾아야 하는 경우

 

✏️ 사용예시

function greet(name: string): string {
  return `안녕, ${name}!`;
}

const name = "Spartan";
const greeting = greet(name);
console.log(greeting);

 

 

4.Array

  • 기본타입에 []가 붙은 형태의 타입

 

✏️ 사용예시

function calculateSum(numbers: number[]): number {
  let sum: number = 0;
  for (let i = 0; i < numbers.length; i++) {
    sum += numbers[i];
  }
  return sum;
}

const testScores: number[] = [90, 85, 78, 92, 88];
const sumScore = calculateSum(testScores);
console.log(`점수의 총합: ${sumScore}`);

 

 

 

5. Tuple

  • 서로 다른 타입의 원소를 순서에 맞게 가질 수 있는 특수한 형태의 배열
  • 배열은 number[], string[] 처럼 같은 타입의 원소만

 

✏️ 사용예시

const person: [string, number, boolean] = ['Spartan', 25, false];
const person2: [string, number, boolean] = [25, 'Spartan', false]; // 오류!

 

 

6. enum

  • 명확하게 관련된 상수 값들을 그룹화하고자 할 때 사용
  • 열거형 데이터 타입
  • enum 안에 있는 각 요소는 값이 설정되어 있지 않으면 기본적으로 숫자 0으로 시작
  • enum 안에 있는 요소에는 number 혹은 string타입의 값만을 할당

 

✏️ 사용예시

enum UserRole {
  ADMIN = "ADMIN",
  EDITOR = "EDITOR",
  USER = "USER",
}

enum UserLevel {
  NOT_OPERATOR, // 0
  OPERATOR // 1
}

function checkPermission(userRole: UserRole, userLevel: UserLevel): void {
  if (userLevel === UserLevel.NOT_OPERATOR) {
    console.log('당신은 일반 사용자 레벨이에요');
  } else {
    console.log('당신은 운영자 레벨이군요');
  } 

  if (userRole === UserRole.ADMIN) {
    console.log("당신은 어드민이군요");
  } else if (userRole === UserRole.EDITOR) {
    console.log("당신은 에디터에요");
  } else {
    console.log("당신은 사용자군요");
  }
}

const userRole: UserRole = UserRole.EDITOR;
const userLevel: UserLevel = UserLevel.NOT_OPERATOR;
checkPermission(userRole, userLevel);

 

 

7. any

  • TypeScript에서 any 타입은 모든 타입의 슈퍼 타입
  • 어떤 타입의 값이든 저장할 수 있다는 의미

 

✏️ 사용예시

let anything: any;
anything = 5; // 최초에는 숫자를 넣었지만
anything = 'Hello'; // 문자열도 들어가고요
anything = { id: 1, name: 'John' }; // JSON도 들어가네요

 

 

8. unknown

  • any 타입과 비슷한 역할을 하지만 더 안전한 방식으로 동작
  • unknown 타입의 변수에도 모든 타입의 값을 저장
  • 다른 타입의 변수에 할당하려면 명시적으로 타입을 확인
  • unkwown 타입의 변수를 다른 곳에서 사용하려면 타입 단언을 통해 타입 보장을 하여 사용
  • 재할당이 일어나지 않으면 타입 안전성이 보장이 되지 않음

 

✏️ 사용예시

let unknownValue: unknown = '나는 문자열이지롱!';
console.log(unknownValue); // 나는 문자열이지롱!

let stringValue: string;
stringValue = unknownValue; // 에러 발생! unknownValue가 string임이 보장이 안되기 때문!
stringValue = unknownValue as string; // Type Assertion(타입 단언)
console.log(stringValue);
  • typeof 키워드를 이용하여 타입 체크를 미리한 후 unknown 타입의 변수를 string 타입의 변수에 할당
let unknownValue: unknown = '나는 문자열이지롱!';
let stringValue: string;

if (typeof unknownValue === 'string') {
  stringValue = unknownValue;
  console.log('unknownValue는 문자열이네요~');
} else {
  console.log('unknownValue는 문자열이 아니었습니다~');
}

 

 

9. union

  • 여러 타입 중 하나를 가질 수 있는 변수를 선언할 때 사용
  • | 연산자를 사용하여 여러 타입을 결합하여 표현
  • TypeScript를 쓰면서 여러 타입을 하나의 변수로 해결하겠다는 생각은 가급적 지양

 

✏️ 사용예시

type StringOrNumber = string | number; // 원한다면 | boolean 이런식으로 타입 추가가 가능해요!

function processValue(value: StringOrNumber) {
  if (typeof value === 'string') {
    // value는 여기서 string 타입으로 간주됩니다.
    console.log('String value:', value);
  } else if (typeof value === 'number') {
    // value는 여기서 number 타입으로 간주되구요!
    console.log('Number value:', value);
  }
}

processValue('Hello');
processValue(42);

 

 

🔗 마이페이지(1)

 

[팀프로젝트] 리액트로 뉴스피드 웹사이트 제작하기 - 마이페이지(1)

🔗 사전기획 [팀프로젝트] 리액트로 뉴스피드 웹사이트 제작하기 - 사전기획리액트로 첫 팀프로젝트가 시작됐다. 뭔가 프로젝트다운 프로젝트를 하는 것 같다. 인스타그램이나 페이스북,  블

ejunyang.tistory.com

 

 

 

 

 

✏️ 내가 쓴 게시글 불러오기

로그인한 아이디와 같은 유저 아이디를 가진 데이터의 모든 정보를 posts 테이블에서 가져온다.

 const { data: postData, error: postError } = await supabase
        .from('posts')
        .select('*')
        .eq('user_id', userData.email);
      setPosts(postData);

      if (postError) {
        console.error('게시글 정보를 가져오지 못했습니다.', postError);
        return;
      }

 

 

❤️ 좋아요 누른 게시글 불러오기

    const { data: likeData, error: likeError } = await supabase
        .from('user_info')
        .select('post_heart')
        .eq('user_id', userData.email);

      if (likeError) {
        console.error('좋아요한 게시글 정보를 가져오지 못했습니다.', likeError);
        return;
      }

 

JSON 형식의 문자열로 저장된 게시글 ID들이 들어있고, 이 문자열을 JSON.parse를 사용하여 JavaScript 배열로 변환한 다음 likedPostIds라는 변수에 저장한다.

 

let likedPostIds = JSON.parse(likeData[0].post_heart);

 

좋아요한 게시글 ID가 담겨있는 likedPostId를 map 을 돌려 새로운 배열을 반환한다. 각 게시물 ID (postId)에 대해 Supabase 데이터베이스에서 해당 ID와 일치하는 게시물의 세부 데이터를 가져온다. likedPostDetails 배열에서 null이 아닌 요소들만 필터링하여 setLikedPosts 함수로 전달한다.

      try {
        const likedPostDetails = await likedPostIds.map(async (postId) => {
            const { data: likedPostData, error } = await supabase
            .from('posts')
            .select('*')
            .eq('id', postId);

            if (error) {
              console.error('좋아요한 게시글의 상세 정보를 가져오지 못했습니다.', error);
              return null;
            }
            return likedPostData;
          })
        setLikedPosts(likedPostDetails.filter((post) => post !== null));
      } catch (error) {
        console.error('오류', error);
      }

2

 

👾 오류

likedPostIds가 배열이 아니라는 타입오류가 났다. 찾아보니 async 함수는 항상 Promise를 반환한다는 것을 알아냈다. 반환된 프로미스 객체를 비동기식으로 처리하기 위래 Promise all 메서드를 사용해보았다.

 

🔗 참고자료

 

Promise.all() 로 비동기 처리를 구현해 보자

- Promise.all() 은 여러 개의 Promise 들을 비동기적으로 실행하여 처리할 수 있다.

velog.io

 

 

 

💡 해결

흐름은 아래와 같다. 아래 흐름으로 간다면 Promise.all이 완료됐을 때, likedPostDetails는 각 postId에 대한 쿼리 결과의 배열이 된다.

  • Promise.all은 전달된 모든 Promise가 해결될 때까지 기다린 후, 그 결과를 배열로 반환
  • likedPostIds.map은 각 postId에 대해 비동기 작업을 수행하는 Promise 객체의 배열을 반환
  • 해당 배열을 Promise.all에 전달하여 모든 비동기 작업이 완료될 때까지 기다림
  • await를 사용하여 모든 Promise가 완료될 때까지 기다림

 

 const likedPostDetails = await Promise.all(
          likedPostIds.map(async (postId) => {
            const { data: likedPostData, error } = await supabase.from('posts').select('*').eq('id', postId);

            if (error) {
              console.error('좋아요한 게시글의 상세 정보를 가져오지 못했습니다.', error);
              return null;
            }
            return likedPostData;
          })
        );
        setLikedPosts(likedPostDetails.filter((post) => post !== null));
      } catch (error) {
        console.error('오류', error);
      }

 

 

 

📌 북마크한 게시글 불러오기

 const { data: saveData, error: saveError } = await supabase
        .from('user_info')
        .select('post_save')
        .eq('user_id', userData.email);
      setSavePosts(saveData);

      if (saveError) {
        console.error('북마크한 게시글 정보를 가져오지 못했습니다.', saveError);
        return;
      }

 

 

saveData[0]가 존재하는지 확인하고, 존재하지 않으면 undefined 처리. 옵셔널 체이닝으로(?) saveData[0]가 존재하지 않으면 undefined를 반환한다. 좋아요 게시글의 세부데이터를 가져오는 방식과 같은 방식이다.

 

let savePostIds = JSON.parse(saveData[0]?.post_save || '[]');
      try {
        const savePostDetails = await Promise.all(
          savePostIds.map(async (postId) => {
            const { data: savedPostData, error } = await supabase.from('posts').select('*').eq('id', postId);

            if (error) {
              console.error('북마크한 게시글의 상세 정보를 가져오지 못했습니다.', error);
              return null;
            }
            return savedPostData;
          })
        );
        setSavePosts(savePostDetails.filter((post) => post !== null));
      } catch (error) {
        console.error('오류', error);
      }
    };

createBrowserRouter

createBrowserRouter는 react router v6.4부터 사용할 수 있다.

 

 

 

라우터 생성

createBrowserRouter()

createBrowserRouter()에 라우팅 할 path element로 작성할 수 있다. children 속성으로 배열에 중첩된 라우터(Nested Router)를 추가해 줄 수 있다.

import { createBrowserRouter } from "react-router-dom";
import Login from "../pages/Login";
import Join from "../pages/Join";
import Layout from "../layout/Layout";
import Main from "../pages/Main";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        path: "/",
        element: <Main />,
      },
      {
        path: "/login",
        element: <Login />,
      },
      {
        path: "/join",
        element: <Join />,
      },
    ],
  },
]);

 

 

 

 

App.jsx

최상단에서 <RouterProvider> 를 import한다.
RouterRrovider에는 router={} props를 필수로 넣어야하고, Router.jsx에서 createBrowserRouter 함수로 생성한 router를 넘겨준다.

import React from "react";
import { RouterProvider } from "react-router-dom";
import { router } from "./router/Router";

const App = () => {
  return <RouterProvider router={router} />;
};

export default App;

 

기존의 상태관리 라이브러리인 Redux는 제공하는 기능과 연계된 미들웨어 등 매우 강력한 퍼포먼스를 자랑하지만, 설정과 사용법이 복잡했다.  Zustand는 상태관리 본연의 기능에 집중하여 위와 같은 복잡성을 줄이고, 보다 간단하고 직관적인 상태관리 기능을 제공한다는 장점이 있다. 

📌 공식문서
단순화된 Flux 패턴을 사용하는 작고(small) 빠르고(fast) 확장가능한(scalable) 상태관리 솔루션이며, 
Hooks에 기반으로하는 간편한 API가 존재

 

 

 

 

설치

yarn add zustand

 

해당 경로로 파일 생성 후 세팅 초기값을 설정하고, 1씩 증가하는 로직과 초기화하는 로직을 만들었다.

// src > zustand > bearsStore.js
import { create } from "zustand";

const useBearsStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

export default useBearsStore;

 

 

 

사용

다른 컴포넌트에서 사용하는 방법은 useState, useEffect와 같은 기본 훅을 사용하는 것처럼 간편하게 사용할 수 있다.

// src > App.jsx
import "./App.css";
import useBearsStore from "./zustand/bearsStore";

function App() {
  const bears = useBearsStore((state) => state.bears);
  const increasePopulation = useBearsStore((state) => state.increasePopulation);
  return (
    <div>
      <h1>{bears} around here ...</h1>
      <button onClick={increasePopulation}>one up</button>
    </div>
  );
}
export default App;

심화프로젝트 사전 기획 중 스타일링은 어떤 라이브러리를 사용할껀지 논의하다가 강민 튜터님께서 next.js를 쓰면 Tailwind css는 자연스럽게 쓰는 추세라고 말씀해주셨다. 거진 고정적이라고.. 그래서 이번 프로젝트 때 테일윈드를 사용해보기로 합의했다. 한번도 써본적 없어서 좀 부담스럽지만 요새 계속 쓰는 추세라고하니 사용해보는게 좋을 것 같다.

 

 

 

TailWind CSS

성능 이슈와 클래스 네임 충돌 이슈를 줄이고, 유연하고 직관적인 스타일링을 제공한다. 공식 문서에서는 

유틸리티 퍼스트(Utility-First) CSS 프레임워크로, 빠르고 쉽게 스타일링을 적용할 수 있는 클래스를 제공한다 고 쓰여져있다.

📌 유틸리티 클래스란?
유틸리티 클래스는 특정 스타일 속성을 나타내는 짧고 간단한 `CSS 클래스`로, 
HTML 요소에 직접 적용하여 빠르고 쉽게 스타일링을 할 수 있습니다.
Tailwind CSS는 이러한 유틸리티 클래스를 대량으로 제공하여, 
별도의 CSS 작성 없이도 다양한 스타일을 구현할 수 있게 합니다. 
예를 들어,`bg-blue-500`은 배경색을 파란색으로, `p-4`는 패딩을 설정하는 유틸리티 클래스입니다.

 

 

 

성능

  • Tailwind CSS는 불필요한 스타일을 제거하고, 필요한 부분만 스타일을 적용하는 방식으로 성능 최적화
  • 사용하지 않는 CSS를 제거하여, 최종 빌드 파일 크기를 최소화
  • CSS 파일의 크기를 줄이고, 애플리케이션 로딩 속도를 개선
  • Tailwind CSS는 React의 JSX 문법과 함께 사용할 수 있어, 스타일링 간편
  • Tailwind 설정 파일(tailwind.config.js)을 통해 색상, 폰트, 스페이싱 등 다양한 설정을 커스터마이징
  • 다양한 설정 옵션을 제공하여, 프로젝트의 요구사항에 맞는 스타일링을 손쉽게 적용
📌 Purging CSS?
`Purging CSS`는 사용하지 않는 CSS를 제거하여 최종 빌드 파일 크기를 줄이는 과정을 의미합니다. 
Tailwind CSS는 이를 자동으로 처리해줍니다.

 

 

 

 

설치 및 기본 사용법

yarn add tailwindcss postcss autoprefixer

 

초기화 파일 생성

yarn tailwind init -p

tailwind.config.js

 

 

 

 

 

 

 

 

 

 

 

 

 

 

tailwind.config.js 수정

src 하위 파일 중 확장자가 .js, .jsx, .ts, .tsx인 파일을 대상으로 한다.

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

 

 

index.css

css에 적용하기

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

 

 

 

 

컴포넌트에 써먹기

inline-style

return (
    <div style={{display:"flex" flexDirection:"column" gap:"8px" marginBottom:"1.25rem"}} >
      {sortedPlace.map((place) => {
        return <PlaceItem key={place.id} place={place} />;
      })}
    </div>
  );

 

 

Tailwind CSS

return (
    <div className="flex flex-col gap-8 mb-20">
      {sortedPlace.map((place) => {
        return <PlaceItem key={place.id} place={place} />;
      })}
    </div>
  );

 

 

 

무조건 껐다 키기!!!!!!!!! 껐다 켜야지 적용된다 적용안된다고 울지마세요 엉엉 난 울었으니까..

 

 

 

🔗 사전기획

 

[팀프로젝트] 리액트로 뉴스피드 웹사이트 제작하기 - 사전기획

리액트로 첫 팀프로젝트가 시작됐다. 뭔가 프로젝트다운 프로젝트를 하는 것 같다. 인스타그램이나 페이스북,  블로그 같이 게시글을 쓰고 읽고 수정하고 삭제할 수 있는 뉴스피드 웹사이트를

ejunyang.tistory.com

 

 

 

 

 

 

마이페이지 구현

마이페이지에 필요한 기능을 정리해보았다.

1. 로그인한 사용자의 데이터 가져오기 => id, nickname, 프로필 이미지
2. 사용자가 쓴 게시글 데이터 가져오기
3. 사용자가 좋아요 누른 게시글 데이터 가져오기
4. 사용자가 저장한 북마크 게시글 데이터 가져오기

 

 

 

컴포넌트 분리

아래와 같이 컴포넌트를 나누었다.

 

 

 

 

사용자 데이터 가져오기

 

준혁님께서 Auth.api 를 만들어주셔서 getUser를 가지고와서 사용했다. 

export const getUser = async () => {
  try {
    const {
      data: { user },
      error
    } = await supabase.auth.getUser();

    if (error) {
      console.log('Error:', error);
      return null;
    }

    // console.log('User data:', user);
    return user;
  } catch (error) {
    console.log('Error:', error);
    return null;
  }
};

 

마이페이지에서 화면에 보여줘야할 정보가 프로필 이미지와 닉네임이었기때문에 프로필 이미지와 닉네임은 useState 를 사용해줬다. 그리고 이번 과제를 하면서 supabase를 처음 다루어보았는데 우리가 Auth 테이블과 member 테이블 두개를 만들어놓은 상태로 개발을 진행한 상태라서 프로필을 변경할 땐 두 테이블 다 데이터를 업데이트해줘야했다. 

 useEffect(() => {
    const fetchData = async () => {
      const userData = await getUser();
      setProfileUrl(userData.user_metadata.imageSrc);
      setUser(userData);
      setNickname(userData.user_metadata.nickname);

      if (!userData) {
        console.error('유저 정보를 가져올 수 없습니다.');
        return;
      }
      const { data: memberData, error: memberError } = await supabase
        .from('member')
        .select('*')
        .eq('user_id', userData.email);

      if (memberError) {
        console.error('회원정보를 가져오지 못했습니다.', memberError);
        return;
      }
    };
    fetchData();
  }, []);

 

 

 

 

프로필 수정하기

 

변경해야할 닉네임은 useRef로 처리해서 DOM에 접근할 수 있도록 했다. state로 처리 하기엔 인풋에 값을 변경할때마다 불필요한 렌더링이 일어나기 때문에 useRef로 처리하는게 좋겠다고 생각했다.

  useEffect(() => {
    const fetchData = async () => {
      const userData = await getUser();
      if (userData) {
        setUser(userData);
        setProfileImg(userData.user_metadata.imageSrc);
        nicknameRef.current ? (nicknameRef.current.value = userData.user_metadata.nickname) : '';
      } else {
        console.error('회원정보를 불러오지 못했습니다.', error);
      }
    };
    fetchData();
  }, []);

 

 

 

 

프로필 업데이트

const handleUpdateData = async (e) => {
    e.preventDefault();
    try {
      const newNickname = nicknameRef.current.value;
      const image = e.target.image.files[0];
      const userData = await getUser();
      const ImageData = await apiImg(image);

      if (!ImageData) {
        throw new Error('이미지 업로드 실패');
      }

      // auth 업데이트
      const { data: authData, error: AuthError } = await supabase.auth.updateUser({
        data: { nickname: newNickname, imageSrc: ImageData }
      });

      if (AuthError) {
        console.error('Auth 업데이트 실패', AuthError.message);
        return;
      }

      console.log('Auth 업데이트 성공', authData);
      setUser(authData);

      // member 테이블 업데이트
      const { data, error } = await supabase
        .from('member')
        .update({ user_name: newNickname, user_imageSrc: ImageData })
        .eq('user_id', userData.email)
        .select('*');

      if (error) {
        console.error('member 업데이트 실패', error.message);
        return;
      }

      console.log('member 업데이트 성공', data);
      navigate(-1);
    } catch (error) {
      console.error('업데이트 실패', error);
    }
  };

 

👾 오ㅗ류

지출내역리스트 코드를 작성하다 마주한 에러이다. 한참 고민하다가 if문 구문 순서를 바꾸어 보았다.

const ExesList = () => {
  const { selectedMonth } = useContext(ExpenseContext);
  const { userInfo } = useContext(AuthContext);

  const {
    data: expenses = [],
    isPending,
    error,
  } = useQuery({
    queryKey: ["expenses"],
    queryFn: getExpense,
  });

  const filteredExpenses = userInfo
    ? expenses.filter(
        (exe) => exe.month === selectedMonth && exe.user === userInfo.id
      )
    : [];

  const totalAmount =
    filteredExpenses && filteredExpenses.length > 0
      ? filteredExpenses.reduce((acc, cur) => {
          return (acc += cur.amount);
        }, 0)
      : 0;
      
      
  if (isPending) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }


  return (
    <div>
      <TotalTitle>
        {`${selectedMonth + 1}월`} 한 달 동안
        <strong>총 {totalAmount}원</strong>
        사용했어요.
      </TotalTitle>
      <ul>
        {filteredExpenses &&
          filteredExpenses.map((exe) => {
            return <ExesItem key={exe.id} exe={exe} />;
          })}
      </ul>
    </div>
  );
};

export default ExesList;

 

 

 

 

💡 해결

const ExesList = () => {
  const { selectedMonth } = useContext(ExpenseContext);
  const { userInfo } = useContext(AuthContext);

  const {
    data: expenses = [],
    isPending,
    error,
  } = useQuery({
    queryKey: ["expenses"],
    queryFn: getExpense,
  });

  const filteredExpenses = userInfo
    ? expenses.filter(
        (exe) => exe.month === selectedMonth && exe.user === userInfo.id
      )
    : [];

  if (isPending) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }

  const totalAmount =
    filteredExpenses && filteredExpenses.length > 0
      ? filteredExpenses.reduce((acc, cur) => {
          return (acc += cur.amount);
        }, 0)
      : 0;

 

이렇게 위치를 바꾸니까 해결됐다. 코드의 흐름을 보니 이 때 expenses 데이터가 아직 로딩되지 않았거나 에러가 발생했다면, filteredExpenses의 계산 과정에서 문제가 발생했던 것.  isPending 및 error 체크를 먼저 수행하도록 변경하면 데이터 로딩 상태와 에러 상태를 먼저 확인하고, 해당 상태에 맞는 UI를 렌더링할 수 있다.

 

따라서 isPending 및 error 체크 구문의 위치를 변경함으로써, 데이터 로딩 상태와 에러 상태를 먼저 확인하고 그에 따른 적절한 UI를 렌더링할 수 있게 되었다. 코드의 안정성과 사용성을 높이는 데 도움이 되는 것 같다!

이번 개인 프로젝트때 주로 사용했던 Tanstack Query에 대해 알아보자. 리액트는 정말 훅이 많다..고 항상 느낀다.. 이번 TIL은 리액트 쿼리에 대해 깊이는 아니지만 내가 이해한만큼 써보려고 한다.

 

 

Tanstack Query❓

💡 fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리

 

서버 상태를 관리하기 위한 라이브러리로, 데이터를 패칭하고 캐싱, 동기화, 무효화 등의 기능을 제공한다. 복잡하고 장황한 코드가 필요하지 않고 리액트 컴포넌트 내부에서 간단하고 직관적으로 API를 사용할 수 있으며 이전보다 비동기 로직을 간편하게 작성하고 유지보수성을 높일 수 있다.

 

 

 

 

 

사용하는 이유

비동기 로직의 복잡성 해결 필요성

기존의 방식은 Fetching 코드를 작성하고 데이터를 담아 둘 상태(useState) 생성, useEffect를 이용해 컴포넌트 마운트시 데이터를 Fetching 한 뒤 상태에 저장하였다. 리액트 쿼리를 사용하면 해당 로직의 복잡함을 해결할 수 있다.

 

서버 상태 관리의 어려움

서버 상태는 클라이언트 상태와 달리 캐싱, 동기화, 재검증 등 관리해야 할 요소가 많아 기존 방법으론 관리가 어려움.

 

 

 

 

주요기능

  • 데이터 캐싱: DB서버에 여러번 동일한 데이터를 요청하지 않고 캐싱하여 데이터를 빠르게 가져온다.
  • 자동 리페칭: 데이터가 변경되었을 때 자동으로 리페칭하여 최신 상태를 유지한다.
  • 쿼리 무효화: 특정 이벤트가 발생했을 때 쿼리를 무효화하고 데이터를 다시 가져온다.

 

데이터를 가져오고, 수정하고, 리프레시한다. 무조건 외우기!!

📌 get / Modify / refresh

get = useQuery
Modify = useMutation
refresh = invalidateQueries

 

 

 

 

라이브러리 설치

yarn add @tanstack/react-query

 

리액트 쿼리를 사용하려면 백엔드 데이터 베이스가 필요해서 json-server로 임시 데이터 베이스를 만들어준다.

yarn add json-server

 

 

 

단축명령어 설정

 

📂package.json에서 아래와 같이 설정해준다. 그러면 yarn json 입력시 서버가 켜진다.

{
  "scripts": {
	"json": "json-server --watch db.json --port 5000",
  }
}

 

단축 명령어 없이 서버를 키는 명령어는 아래와 같다.

json-server --watch db.json

 

 

 

 

db.json

📂src > 📄db.json

{
  "expenses": [
    {
      "id": "f70630fd-852d-4566-a830-9d318c22046a",
      "date": "2024-06-18",
      "item": "📚",
      "amount": 50000,
      "desc": "리액트",
      "month": 5,
      "user": "wnswns"
    },
  ]
}

 

 

 

 

QueryClient 생성

useQuery를 사용할 곳에 쿼리 클라이언트를 생성해준 뒤 Provider를 적용해준다. 이렇게하면 전역에서 사용할 수 있다.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

 

 

 

 

데이터 가져오기 : useQuery()

useQuery에 쿼리 키비동기 함수를 인자로 받아 데이터를 가져오고, 로딩 상태, 오류 상태, 그리고 데이터를 반환한다.

여기서 쿼리키는 배열로 들어간다. 

useQuery({ queryKey: [쿼리키], queryFn: 비동기 함수 })

 

실무에서 자주 사용하는 쿼리키 이름

💡 꼭 알아야 하는 것
 - 다른 API는 다른 쿼리키를 사용한다. (ex. 목록 API와 상세 API는 다른 쿼리키를 사용한다)
 - 목록 페이지에선 `["posts"]` or `["movies"]` or `["todos"]`이렇게 쓰는 경우가 많음.
 - 상세 페이지에서는 `["posts", id]` 이렇게 쓰는 경우가 많음.
 - 페이지네이션의 쿼리키는 `["posts", page]` 이렇게 쓰는 경우가 많음

 

 

 

 

서버에서 데이터를 가져오는 API 함수를 만들어보자. 

import axios from "axios";

const JSON_SERVER_HOST = "http://localhost:4000";

export const getExpense = async () => {
  try {
    const response = await axios.get(`${JSON_SERVER_HOST}/expenses`);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 가져오지 못했습니다.");
  }
};

 

 

위에서 만든 API 함수를 useQuery에 적용해보자. 아래 코드로 적용하면 데이터 가져오는 로직 완성.

useQuery({ queryKey: ["expense"], queryFn: getExpense })

 

위 코드를 아래 코드처럼 쓸 수 있다.

  • data : 화면에 보여줄 데이터 (queryFn의 return 값)
  • isLoading : 데이터 로딩 여부 (true, false)
  • error : 데이터 로딩 중 발생한 에러 데이터
const { data, isLoading, error } = useQuery({
  queryKey: ["expense"],
  queryFn: getExpense,
});

 

데이터를 받아오기 전 로딩중으로 뜨고, 데이터를 가져오지 못할 경우 예외 처리를 해줄 수 있다.

  if (isLoading) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }

 

 

 

 

데이터 추가, 수정, 삭제하기 : useMutation()

export const postExpense = async (newExes) => {
  try {
    const response = await axios.post(`${JSON_SERVER_HOST}/expenses`, newExes);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 생성하지 못했습니다.");
  }
};

 

 

다른 컴포넌트에서 리액트 쿼리를 써야할 경우 ✔️ useQueryClient() 훅을 사용해줘야한다. invalidateQueries 속성을 사용해주기 위해선 필수이다. 생성 후 데이터를 다시 가져와야하기 때문에 invalidateQueries를 사용해줬다. 기존에 있던(데이터를 생성하기전) 데이터는 무효화시키고 최신 상태(데이터가 추가된)를 가져온다

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: postExpense,
    onSuccess: () => {
      queryClient.invalidateQueries(["expenses"]);
    },
  });

 

 

새로운 데이터가 추가되는 함수에 mutation.mutate 사용해주면 추가하는 로직도 완성이다. 차례대로 수정과 삭제도 같은 방법으로 하면 된다!

const onInsert = useCallback(
    (date, item, amount, desc) => {
      const newExes = {
        id: uuidv4(),
        date,
        item,
        amount,
        desc,
        month: selectedMonth,
        user: userInfo.id,
      };

      mutation.mutate(newExes);
    },
    [mutation, userInfo]
  );

+ Recent posts