[프로그래머스] 프론트엔드 심화: 상태 관리와 비동기 처리(1)

Lina Hongbi Ko·2024년 11월 4일
0

Programmers_BootCamp

목록 보기
48/76
post-thumbnail

2024년 11월 4일

✏️ 프로젝트 시작

  • 프로젝트 생성

    • npm init vite
      • 현재 프로젝트 이름으로 진행 : ./
      • React
      • TypeScript
    • npm install
      • pakage.json에 있는 dependencies 설치
    • npm run dev
      • 시작
      • scipts에 dev라 되어 있으니 dev을 씀 (start로 바꾸면 npm run start라고 할 수 있음)
    • htps://localhost:5173/ 에서 프로젝트 확인 가능
  • localhost:4000/about : SPA → 싱글 페이지에서 진행

  • about.html : MPA

✏️ 구조 생성

  • 폴더 생성 하기

    • components → 컴포넌트 관리
    • hooks → 커스텀 훅 관리
    • store → 상태 관리 (라이브러리 이용 → Redux, Mobx, Zustand, Recoil, React-Context)
    • types → 타입 관리

✏️ 필요한 패키지들 설치

  • @reduxjs/toolkit

    • redux
    • react-redux
  • clsx : classname을 만들때 동적으로 만들기 위해 사용

  • @vanila-extract/css

    • @vanilla-extract/css-utils
    • @vanilla-extract/vite-plugin
  • react-icons

  • uuid

  • react-beautiful-dnd : 드래그앤드랍 기능을 위해 사용

  • 설치하기

    • npm install @reduxjs/toolkit redux react-redux clsx @vanilla-extract/css @vanilla-extract/css-utils @vanilla-extract/vite-plugin react-icons uuid react-beautiful-dnd

✏️ 리덕스 사용 준비

  • 리덕스

    • 상태 관리 라이브러리
    • 원래는 state, props를 통해 상태를 여러 컴포넌트와 공유했음
    • 어플리케이션이 커지면 관리 힘듦 & 소스코드 지저분
  • 리덕스의 흐름(Flow)

    • Action 객체 Dispatch 함수 ⇒ Reducer 함수 type return ⇒ Redux Store State ⇒ React Component Rerendering
  • Redux toolkit을 사용해서 Reducer을 생성하려면 Slice가 있어야함

  • Toolkit → Slice → Reducer → Store → Provider → react component wrapping

  • Slice 만들기(Sub reducer 만들기)

// types / index.ts

export interface ITask {
  taskId: string;
  taskName: string;
  taskDescription: string;
  taskOwner: string;
}

export interface ILogItem {
  logId: string;
  logAuthor: string;
  logMessage: string;
  logTimeStamp: string;
}
// store / slices / modalSlice.ts

import { createSlice } from "@reduxjs/toolkit";
import { ITask } from '../../types';

type TModalState = {
  boardId: string;
  listId: string;
  task: ITask;
}

const initialState : TModalState = {
  boardId: "board-0",
  listId: "list-0",
  task: {
    taskId: "task-0",
    taskName: "task 0",
    taskDescription: "task description",
    taskOwner: "Lina"
  }
};

const modalSlice = createSlice({
  name: 'modal',
  initialState,
  reducers: {

  }

});

export const modalReducer = modalSlice.reducer;
// store / slices / loggerSlice.ts

import { createSlice } from "@reduxjs/toolkit";
import { ILogItem } from '../../types';

type TloggerState = {
  logArray : ILogItem[],
}

const initialState : TloggerState = {
  logArray: []
}

const loggerSlice = createSlice({
  name: 'logger',
  initialState,
  reducers: {

  }
})

export const loggerReducer = loggerSlice.reducer;
// store / slices / boardsSlice.ts
 
 import { createSlice } from "@reduxjs/toolkit";
 
 const initialState = {
	 modalActive: false,
	 boardArray: []
 };
 
 const boardsSlice = createSlice({
	 name: 'boards',
	 initialState,
	 reducers : {
	 
	 }
 })
 
 export const boardsReducer = boardsSlice.reducer;
  • ub reducer들을 만들면 합쳐줘야 함 → 하나의 메인 리듀서로 합치기

  • reducer combine

// store / reducer / reducer.ts

import { boardsReducer } from '../slices/boardsSlice';
import { loggerReducer } from '../slices/loggerSlice';
import { modalReducer } from '../slices/modalSlice';

const reducer = {
  logger: loggerReducer,
  boards: boardsReducer,
  modal: modalReducer
};

export default reducer;
  • export default vs export

    • export default → 중괄호{ } 없이 루트만 맞으면 어떤 이름으로 가져와도 됨(파일당 하나만 export 가능)
    • export → 중괄호{ } 쓰고 그 이름으로 가져와야 함(파일당 여러개 export 가능)
  • store 생성 → 이제 데이터를 가져올 곳을 마련해야함

// store / index.ts

import { configureStore } from '@reduxjs/toolkit';
import reducer from './reducer/reducer';

const store = configureStore({
  reducer // reducer : reducer
})

export default store;
  • store 연결
// main.tsx

import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { Provider } from 'react-redux'
import store from './store/index.ts'

createRoot(document.getElementById('root')!).render(
  <Provider store={store}>
    <App />
  </Provider>
)
... 생략 ...

Board 데이터 및 타입 생성 (boardSlice 본격 생성)

💡 Board 데이터 및 타입 생성 (boardSlice 본격 생성)

// store / slice / boardsSlice.ts

import { createSlice } from "@reduxjs/toolkit";
import { IBoard } from '../../types';

type TBoardsState = {
  modalActive : boolean;
  boardArray: IBoard[];
}

const initialState : TBoardsState = {
  modalActive: false,
  boardArray: [
    {
      boardId : 'board-0',
      boardName : '첫 번째 게시물',
      lists : [
        {
          listId: "list-0",
          listName: "List 1",
          tasks : [
            {
              taskId: "task-0",
              taskName : "Task 1",
              taskDescription : "Description",
              taskOwner: "Lina",
            },
            {
              taskId: "task-1",
              taskName : "Task 2",
              taskDescription : "Description",
              taskOwner: "Lina",
            },
          ]
        },
        {
          listId: "list-1",
          listName: "List 2",
          tasks : [
            {
              taskId: "task-2",
              taskName : "Task 3",
              taskDescription : "Description",
              taskOwner: "Lina",
            }
          ]
        }
      ]
    }
  ]
}

const boardsSlice = createSlice({
  name: 'boards',
  initialState,
  reducers : {

  }
})

export const boardsReducer = boardsSlice.reducer;
// types / index.ts

export interface ITask {
  taskId: string;
  taskName: string;
  taskDescription: string;
  taskOwner: string;
}

export interface ILogItem {
  logId: string;
  logAuthor: string;
  logMessage: string;
  logTimeStamp: string;
}

export interface IBoard {
  boardId: string;
  boardName: string;
  lists: IList[];
}

export interface IList {
  listId: string;
  listName: string;
  tasks: ITask[];
}

✏️ 리덕스 Hooks 생성

  • 리덕스 스토어 데이터 가져오기 : userSelector()

  • 데이터 업데이트(action dispatch) : useDispatch()

  • 타입스크립트에서 추론을 하지 못하면 개발자가 타입을 지정(annotate)

  • store에서 데이터를 가지고 옴 → store.getState() 사용 → 타입을 가져올 것임

  • 리턴 유틸리티 타입

    • type A = ReturnType<() ⇒ string>
      • 함수의 리턴하는 ‘타입’이 type A로 들어감 (type A = string)
      • 따라서 store.getState()의 리턴하는 타입을 지정해주려면
      • type RootState = ReturnType
// store / index.ts

import { configureStore } from '@reduxjs/toolkit';
import reducer from './reducer/reducer';

const store = configureStore({
	reducer
});

// store의 데이터 타입 지정
// 리턴 유틸리티 타입
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch;

// 이제 state의 타입을 지정할 수 있게 되었고, 아래 처럼 사용할 수 있다.
const logger = useSelector((state: RootState) => state.logger);
// const usingDispatch = useDispatch((dispatch : AppDispatch) => ~~)

// 그런데, 매번 useSelector()을 사용해 RootState를 export 해서 가져오긴 힘드니까
// useDispatch도 마찬가지.
// useAppSelector or useTypedSelector / useTypedDispatch
// 처럼 Hooks를 새롭게 만들고 RootState, AppDispatch를 내장해서 만들면
// RootState, AppDispatch를 계속 가져오지 않아도 됨!
  • 훅 만들기
// hooks / redux.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { RootState } from '../store';

// const useTypedSelector : RootState = useSelector; // 오류생김
// 아래처럼 바꿔줘야함
const useTypedSelector : TypedUseSelectorHook<RootState> = useSelector;
const logger = useSelector((state : RootState) => state.logger);
// TypedUseSelectorHook을 쓰고 제네릭으로 감싸준 이유는,
// state : RootState를 보았을 때
// RootState는 현재
/*
	type RootState = {
		logger : TLoggerState;
		boards : TBoardsState;
		modal : TModalState;
	}
*/
// 이다. 그러나 state는
/*
	state : {
		logger : TLoggerState;
		boards : TBoardsState;
		modal : TModalState;
	}
*/
// 으로 다르다. state : {} 부분이 RootState에는 없음
  • 비슷한 예시를 보면,
interface State {
	state : {
		data: string,
		loading: boolean
	}
}

const obj:State = {
	name : { // name이 없어서 에러 발생
		state: {
			data: 'abcd',
			loading: false
		}
	}
}

// 이와 비슷한 상황임
  • 이를 해결하기 위해서 제네릭 사용
// 그렇기 때문에 제네릭을 사용해준다.

interface Obj<T> {
	name: T;	
}

interface State {
	state : {
		data: string,
		loading: boolean
	}
}

const obj:Obj<State> = {
	name : { 
		state: {
			data: 'abcd',
			loading: false
		}
	}
}
  • TypedUseSelectorHook의 역할이 제네릭과 같은 역할임
// hooks / redux.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../store';

export const useTypedSelector : TypedUseSelectorHook<RootState> = useSelector;

// dispatch도 바꿔주기
export const useTypedDispatch = () => useDisaptch<AppDispatch>();

// 타입을 넣지 않고 위의 훅을 넣어서 아래처럼 편리하게 사용 가능
const logger = useTypedSelector((state) => state.logger);

✏️ 전역 스타일 생성

  • vanilla extract을 통해 전역 스타일 설정
// vite.config.ts

// vanilla extract 플러그인 등록

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), vanillaExtractPlugin()],
})
  • 전역 스타일 만들어주기
// App.css.ts
import { createGlobalTheme, style } from '@vanilla-extract/css';

export const vars = createGlobalTheme(":root", {
  color: {
    main : "#ffa726",
    mainDarker : "#f57c00",
    mainFaded: "#ffb74d",
    mainFadedBright: "#ffb74da6",
    list : "rgb(235, 236, 240)",
    task : "rgb(255, 255, 255)",
    taskHover : "rgb(245, 245, 245)",
    brightText : "rgb(255, 255, 255)",
    darkText : "rgb(24, 42, 77)",
    secondaryDarkText : "rgb(94, 108, 132)",
    secondaryDarkTextHover : "rgb(218, 219, 226)",
    selectedTab : "rgb(137, 176, 174)",
    updateButton : "rgb(237, 180, 88)",
    deleteButton : "rgb(237, 51, 88)",
  },
  fontSizing: {
    T1 : "32px",
    T2 : "24px",
    T3 : "18px",
    T4 : "14px",
    P1 : "12px"
  },
  spacing: {
    small : "5px",
    medium : "10px",
    big1 : "20px",
    big2 : "15px",
    listSpacing : "30px" 
  },
  font: {
    body: "arial",
  },
  shadow: {
    basic : "4px 4px 8px 0px rgba(34, 60, 80, 0.2)",
  },
  minWidth : {
    list: "250px"
  }
});

// 사용 : vars.color.main

export const appContainer = style({
  display : "flex",
  flexDirection : "column",
  minHeight : "100vh",
  height: "max-content",
  width: "100vw"
});

export const board = style({
  display: "flex",
  flexDirection : "row",
  height : "100%",
})

export const buttons = style({
  marginTop : "auto",
  paddingLeft : vars.spacing.big2
})
// App.tsx


import { useState } from 'react'
import reactLogo from './assets/react.svg'
import { appContainer, board, buttons } from './App.css'


function App() {

  return (
    <div className={appContainer}>
      <div className={board}>
        
      </div>
      <div className={buttons}>
          <button>
            이 게시판 삭제하기
          </button>
          <button>

          </button>
      </div>
    </div>
  )
}

export default App

🍎🍏 오늘의 느낀점 : 드디어 프로젝트에 들어가서 리덕스를 셋팅과 vanilla extract을 사용해봤다. 제네릭 문법을 대충 알고 있었는데 다시 찾아보면서 개념을 익혔다. 타입스크립트에 대해 좀 더 공부가 필요할 것 같다.

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글