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

Lina Hongbi Ko·2024년 11월 7일
0

Programmers_BootCamp

목록 보기
51/76
post-thumbnail

2024년 11월 7일

✏️ React Beautiful Dnd를 이용한 간단한 앱 만들기

  • 프로젝트 생성

    • npx create-react-app ./
    • npm run start
  • drag and drop 라이브러리 사용

    • npm i react-beautiful-dnd

  • 각 컴포넌트는 어떤 부분을 담당할까?
    • DragDropContext : 전체를 감쌈_드래그앤드랍을 할 부분을 사용하고 싶은 부분을 감쌈 (wraps the part of your application you want to have drag and drop enabled for)
    • Droppable : 떨어뜨리는 부분을 감쌈 (an area that can be dropped into. contains )
    • Draggable : 드래그할 아이템 (what can be dragged around)
// App.js

import React, { useState } from "react";
import "./App.css";
import { DragDropContext } from "react-beautiful-dnd";
import { Droppable } from "react-beautiful-dnd";
import { Draggable } from "react-beautiful-dnd";

const finalSpaceCharacters = [
  {
    id: "gary",
    name: "Gary Goodspeed",
  },
  {
    id: "cato",
    name: "Little Cato",
  },
  {
    id: "kvn",
    name: "KVN",
  },
];

function App() {
  const [characters, setCharacters] = useState(finalSpaceCharacters);
  const handleEnd = (result) => {
    // result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함
    console.log(result);

    // 목적지가 없으면 이 함수를 종료합니다.
    if (!result.destination) return;

    // 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
    const items = Array.from(characters);

    // 1. 변경 시키는 아이템을 배열에서 지워주기
    // 2. return 값으로 지워진 아이템을 반환함
    const [reorderedItem] = items.splice(result.source.index, 1);
    console.log(reorderedItem); // 지운 아이를 반환 & dropend후이니깐, result.destination.index의 순서가 바뀜

    // 원하는 자리(result.destination.index)에 reorderedItem을 insert 해줍니다.
    items.splice(result.destination.index, 0, reorderedItem);
    setCharacters(items);
  };
  return (
    <div className="App">
      <header className="App-header">
        <h1>Final Space Characters</h1>
        <DragDropContext onDragEnd={handleEnd}>
          <Droppable droppableId="characters">
            {(provided) => (
              <ul
                className="characters"
                {...provided.droppableProps}
                ref={provided.innerRef}
              >
                {characters.map(({ id, name }, index) => {
                  return (
                    <Draggable key={id} draggableId={id} index={index}>
                      {(provided) => (
                        <li
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                        >
                          <p>{name}</p>
                        </li>
                      )}
                    </Draggable>
                  );
                })}
                {provided.placeholder}
              </ul>
            )}
          </Droppable>
        </DragDropContext>
      </header>
    </div>
  );
}

export default App;

✏️ DragAndDrop 기능 만들기 시작

  • npm i --save-dev @types/react-beautiful-dnd
// App.tsx

... 생략 ...
import { DragDropContext } from 'react-beautiful-dnd';

function App() {
  
  ... 생략 ...
  
  const handleDragEnd = () => {

  }
  return (
  
    ... 생략 ...
    
      <div className={board}>
        <DragDropContext onDragEnd={handleDragEnd}>
          <ListsContainer lists={lists} boardId={getActiveBoard.boardId}/>
        </DragDropContext>
      </div>
      
    ... 생략 ...
// components / List / List.tsx

... 생략 ...

import { Droppable } from 'react-beautiful-dnd';

... 생략 ...

const List: FC<TListProps>= ({
  list,
  boardId
}) => {
  ... 생략 ...
  return (
    <Droppable droppableId={list.listId}>
      {provided => (
        <div 
          {...provided.droppableProps}
          ref={provided.innerRef}
          className={listWrapper}>
          <div className={header}>
            <div className={name}>{list.listName}</div>
            <GrSubtract className={deleteButton} onClick={() => handleListDelete(list.listId)}/>
          </div>
          {
            list.tasks.map((task, index) => (
            <div
              onClick={() => handleTaskChange(boardId, list.listId, task.taskId, task)}
              key={task.taskId}
            >
              <Task 
                taskName={task.taskName}
                taskDescription={task.taskDescription}
                boardId={boardId}
                id={task.taskId}
                index={index}
              />
            </div>))
          }
          {provided.placeholder}
          <ActionButton
            boardId={boardId}
            listId={list.listId}
          />
        </div>
      )}
    </Droppable>
  )
}

export default List
// components / Task / Task.tsx

... 생략 ...

import { Draggable } from 'react-beautiful-dnd';

... 생략 ...

const Task: FC<TTaskProps> = ({
  index,
  id,
  boardId,
  taskName,
  taskDescription
}) => {
  return (
    <Draggable draggableId={id} index={index}>
      {provided => (
        <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} className={container}>
          <div className={title}>{taskName}</div>
          <div className={description}>{taskDescription}</div>
        </div>
      )}
      
    </Draggable>
  )
}

export default Task

✏️ DragAndDrop 기능 생성

// App.tsx

... 생략 ...
import { DragDropContext } from 'react-beautiful-dnd';

function App() {
  
  ... 생략 ...
  
  const handleDragEnd = (result: any) => {
      const {destination, source, draggableId} = result;
      const sourceList = lists.filter((list) => list.listId === source.droppableId)[0];

      dispatch(
        sort({
          boardIndex: boards.findIndex(board => board.boardId === activeBoardId),
          droppableIdStart: source.droppableId,
          droppableIdEnd: destination.droppableId,
          droppableIndexStart: source.index,
          droppableIndexEnd: destination.index,
          draggableId: draggableId // (task 어떤걸 전달하는지)
        })
      );

      dispatch(
        addLog({
          logId: uuidv4(),
          logMessage : `
          리스트 "${sourceList.listName}"에서
          리스트 "${lists.filter(list => list.listId === destination.droppableId)[0].listName}"으로
          "${sourceList.tasks.filter(task => task.taskId === draggableId)[0]}"을 옮김."`,
          logAuthor : "User",
          logTimeStamp : String(Date.now())
        })
      );
  }
  return (
  
    ... 생략 ...
    
      <div className={board}>
        <DragDropContext onDragEnd={handleDragEnd}>
          <ListsContainer lists={lists} boardId={getActiveBoard.boardId}/>
        </DragDropContext>
      </div>
      
    ... 생략 ...
// store / slice / boardSlice.ts

... 생략 ...

type TSortAction = {
  boardIndex: number;
  droppableIdStart: string;
  droppableIdEnd: string;
  droppableIndexStart: number;
  droppableIndexEnd: number;
  draggableId: string
}

... 생략 ...

const boardsSlice = createSlice({
  name: 'boards',
  initialState,
  reducers :
  
  ... 생략 ...
  
  sort : (state, {payload}: PayloadAction<TSortAction>) => {

    }
  }
})

export const { sort, addBoard, deleteBoard, deleteList, setModalActive, addList, addTask, updateTask, deleteTask} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;

✏️ sort 로직 생성

// store / slice / boardsSlice.ts

... 생략 ...

sort : (state, {payload}: PayloadAction<TSortAction>) => {
      // same list
      if(payload.droppableIdStart === payload.droppableIdEnd) {
        const list = state.boardArray[payload.boardIndex].lists.find(
          list => list.listId === payload.droppableIdStart
        )

        // 변경시키는 아이템을 배열에서 지워주고
        // return 값으로 지워진 아이템을 줌.
        const card= list?.tasks.splice(payload.droppableIndexStart, 1);
        list?.tasks.splice(payload.droppableIndexEnd, 0, ...card!);
      }

      // other list
      if(payload.droppableIdStart !== payload.droppableIdEnd) {
        const listStart = state.boardArray[payload.boardIndex].lists.find(
          list => list.listId === payload.droppableIdStart
        )

        const card = listStart!.tasks.splice(payload.droppableIndexStart, 1);
        
        const listEnd = state.boardArray[payload.boardIndex].lists.find(
          list => list.listId === payload.droppableIdEnd
        )
        listEnd?.tasks.splice(payload.droppableIndexEnd, 0, ...card);
      }  
    }
  }
  ... 생략 ...

✏️ Firebase 연결하기

  • Firebase : 데이터베이스 ,알림, 스토리지, 배포 서비스 등 많은 것들을 제공함 (우리는 인증을 사용할 것임)
    • Firebase 데이터베이스는 실시간임 → 들어오는 데이터를 실시간으로 구독하는 사람들은 바로 데이터를 가져올 수 있음
  • https://console.firebase.google.com/u/0/?hl=ko
    • 프로젝트 생성
    • 파이어베이스 설치 : npm i firebase
    • firebase.ts (새로운 파일) 만들어서 연결 (아래의 내용 입력해서 app 연결해줘야함)

✏️ Login 기능 구현

// components / BoardList / BoardList.tsx

... 생략 ...

import { GoSignOut } from 'react-icons/go'; 
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { app } from '../../firebase';

... 생략 ...

  const auth = getAuth(app);
  const provider = new GoogleAuthProvider();

  const handleLogin = () => {
    signInWithPopup(auth, provider) // 팝업 뜨게함 -> 로그인 하고나서
    .then(userCredential => { // 그 다음 과정은 여기서 처리
      // 구글로 로그인한 사람의 정보가 userCredential에 있음
      console.log(userCredential);
    })
  }
  
  ... 생략 ...
  
        <GoSignOut className={addButton} />
        <FiLogIn className={addButton} onClick={handleLogin}/>
      </div>
    </div>
  )
}

export default BoardList
  • firebase → authentication → 시작하기 → 원하는 로그인 계정 선택

✏️ Redux Store에 유저 데이터 넣기

// components / BoardList / BoardList.tsx

... 생략 ...

import { GoSignOut } from 'react-icons/go'; 
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { app } from '../../firebase';
import { setUser } from '../../store/slices/userSlice';

... 생략 ...

  const dispatch = useTypedDispatch();
  const auth = getAuth(app);
  const provider = new GoogleAuthProvider();

  const handleLogin = () => {
    signInWithPopup(auth, provider) // 팝업 뜨게함 -> 로그인 하고나서
    .then(userCredential => { // 그 다음 과정은 여기서 처리
      // 구글로 로그인한 사람의 정보가 userCredential에 있음
      console.log(userCredential);
      dispatch(
        setUser({
          email: userCredential.user.email,
          id: userCredential.user.uid,
        })
      );
    })
    .catch(error => {
      console.error(error);
    })
  }
  
  ... 생략 ...
// store / slices / userSlice.ts

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  email: '',
  id: ''
}

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUser : (state, action) => {
      state.email = action.payload.email;
      state.id = action.payload.id;
    }
  }
})

export  const { setUser } = userSlice.actions;
export const userReducer = userSlice.reducer;
// store / reducer / reducer.ts

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

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

export default reducer;

✏️ LogOut 기능 구현

// components / BoardList / BoardList.tsx

... 생략 ...

const dispatch = useTypedDispatch();
  const auth = getAuth(app);
  const provider = new GoogleAuthProvider();

  const { isAuth } = useAuth();
  // console.log(isAuth);

  const handleLogin = () => {
    signInWithPopup(auth, provider) // 팝업 뜨게함 -> 로그인 하고나서
    .then(userCredential => { // 그 다음 과정은 여기서 처리
      // 구글로 로그인한 사람의 정보가 userCredential에 있음
      // console.log(userCredential);
      dispatch(
        setUser({
          email: userCredential.user.email,
          id: userCredential.user.uid,
        })
      );
    })
    .catch(error => {
      console.error(error);
    })
  }
  const handleSignOut = () => {
    signOut(auth)
    .then(() => {
      dispatch(
        removeUser()
      );
    })
    .catch((error) => {
      console.error(error);
    })
  }
  
  ... 생략 ...
  
	    {
          isAuth ? <GoSignOut className={addButton} onClick={handleSignOut}/>
          :
          <FiLogIn className={addButton} onClick={handleLogin}/>
        }
      </div>
    </div>
  )
}

export default BoardList
// hooks / useAuth.ts

import { useTypedSelector } from './redux'

export function useAuth() {
  
  const {id, email} = useTypedSelector((state) => state.user);

  return {
    isAuth : !!email,
    email,
    id
  }
}
// stoer / slices / userSlice.ts 

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  email: '',
  id: ''
}

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUser : (state, action) => {
      state.email = action.payload.email;
      state.id = action.payload.id;
    },
    removeUser : (state) => {
      state.email = '';
      state.id = '';
    }
  }
})

export  const { setUser, removeUser } = userSlice.actions;
export const userReducer = userSlice.reducer;

✏️ 배포

  • AWS, Vercell, github 등을 이용해서 배포 가능

  • Firebase를 이용해서 배포 예정

    • 깃허브 저장소에 먼저 올려야함
      • repository 생성
      • 터미널 입력
        echo "# react-task-app" >> README.md
        git init
        git add README.md
        git commit -m "first commit"
        git branch -M main
        git remote add origin https://github.com/HongbiKo/react-task-app.gitgit push -u origin main
        git add .
        git commit -m "initial"
        git push
  • firebase 서비스를 이용해야함

    • firebase tools 라는 모듈을 전역으로 설치해줘야함

      • npm install -g firebase-tools
        • firebase CLI(Command Line Interface) 입력할 수 있음 → 명령어를 입력해 컴퓨터를 조작하는 방식
    • 사용

      • firebase login
        • 로그인하라고함
  • 리액트앱 같은 경우, 배포를 할때 웹팩으로 파일들을 번들링해서 빌드 파일을 만듦

    • 빌드하기

      • npm run build
    • 빌드가 완료 되면 → dist 폴더 생김 → 이 폴더 안의 파일들만 이용해서 배포할 것임

  • firebase init

    • Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
    • Use an existing project
    • react-test-app-7e790 (react-test-app) (생성판 프로젝트 선택)
    • What do you want to use as your public directory? dist
    • Configure as a single-page app (rewrite all urls to /index.html)? Yes
    • Set up automatic builds and deploys with GitHub? Yes
    • File dist/index.html already exists. Overwrite? No
    • For which GitHub repository would you like to set up a GitHub workflow? HogbiKo/react-task-app
      • 깃허브 저장소와 파이어베이스 저장소를 연결해줄 것인가임(깃허브 저장소 변경되면, 파이어베이스에 바로 deploy 됨)
    • Set up the workflow to run a build script before every deploy? npm install && npm run build
      • 매번 deploy 하기전, 어떠한 스크립트를 사용할 것인가
    • Set up automatic deployment to your site's live channel when a PR is merged? Yes
    • What is the name of the GitHub branch associated with your site's live channel? main
      • 어떤 브랜치에 올릴 것인가
    • Firebase initialization complete! → 파이어베이스 관련 파일 생김
  • 깃에 푸시

    • git add .
    • git commit -m “deploy”
    • git push
  • 오류 상황

// firebase-hosting-merge.yml

# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on merge
on:
  push:
    branches:
      - main
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      // 아래 run 부분 넣어야함
      - run: npm install && npm run build
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_REACT_TEST_APP_7E790 }}
          channelId: live
          projectId: react-test-app-7e790
// firebase-hosting-pull-request.yml

# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on PR
on: pull_request
permissions:
  checks: write
  contents: read
  pull-requests: write
jobs:
  build_and_preview:
    if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      // 아래 run 부분 넣어야함
      - run: npm install && npm run build
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_REACT_TEST_APP_7E790 }}
          projectId: react-test-app-7e790
//  firebase.json

{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ],
    // 이부분 넣어야함
    "predeploy": [
      "npm run build"
    ]
  }
}
  • 다시 git add, commit, push 하고 github actions에 보면 배포된 url로 들어가서 확인 가능

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

0개의 댓글