connecting dots

Beginner 7회차 (8/2) | 데이터 변경에 따른 화면 갱신을 처리하는 두 가지 방법, 낙관적 업데이트, 서버에서 데이터가 수정되었을 때/삭제되었을 때 화면 갱신 실습, 프로젝트 버전관리 시작하기 본문

Live Class/Beginner

Beginner 7회차 (8/2) | 데이터 변경에 따른 화면 갱신을 처리하는 두 가지 방법, 낙관적 업데이트, 서버에서 데이터가 수정되었을 때/삭제되었을 때 화면 갱신 실습, 프로젝트 버전관리 시작하기

dearsuhyun 2024. 8. 3. 15:20

데이터 변경에 따른 화면 갱신 두 가지 방법으로 처리해보기

첫 번째 방법
1) 수정할 데이터를 서버로 전송 (성공)
--> 수정한 데이터의 결과를 응답해주는데

2-1) 응답 받은 결과로 화면을 갱신 (수정된 부분만 갈아끼우는 것 !)

 

updateTodo() --> 서버로 업데이트 전송
 async function updateTodo() {
    console.log('서버로 전송', title)
    const res = await fetch(
      `https://asia-northeast3-heropy-api.cloudfunctions.net/api/todos/${todo.id}`,
      {
        method: 'PUT',
        headers: {
          'content-type': 'application/json',
          apikey: '5X8Z1k7M2vU5Q',
          username: 'Grepp_KDT4_ParkYoungWoong'
        },
        body: JSON.stringify({
          title,
          done: todo.done
        })
      }
    )
    const data = await res.json()​

받아온 데이터가 있으면 업데이트 성공했다는 것. 이때 들고오는 데이터는 '수정한 그 할일 객체'
res 객체를 json으로 파싱하면 도착한 이 data는 수정한 todo 객체가 됨
수정한 객체가 있으니, 수정한 그 항목 부분만 찾아서 덮어써버리면 됨 !

--> 2번을 갈아끼우려면 2번이 속한 전체목록을 수정해야 해야 함 --> 배열 데이터를 손 봐야함
const numbers = [1, 2, 3, 4]
numbers[1] = 9
// [1, 9, 3, 4]​​

 

 

// .map: 배열 아이템 (todo) 갯수에 맞게 콜백함수 호출 
// --> map 메소드의 각각 콜백에 반환되는 데이터로 새로운 배열 데이터를 만드는 용도

const todos = [
//   { id: "a", title: "A" },
//   { id: "b", title: "B" },
//   { id: "c", title: "C" },
//   { id: "d", title: "D" },
// ];
// const undatedTodo = { id: "b", title: "X" };

// [
//   {id: 'a', title: 'A'},
//   {id: 'b', title: 'X'},
//   {id: 'c', title: 'C'},
//   {id: 'd',title: 'D'}
// ]

 

 

--> 1번 방식과 2번 방식의 차이점은 ?

1번

1번 방식의 경우 updatedTodo에 넣어서 전달하는 것 말고 바로 코드를 setTodo에 넣는 방식으로 해도 됨

 

--> todos를 밖에서 가지고와서 사용해야 함 (setTodos의 영역 밖에서 데이터를 가지고 와서 써야 함)

--> setTodos는 독립적인 함수가 아님

 

2번

 

--> map 메소드를 동작시키는 todos는 콜백함수 형태로 해서 setTodos 함수 호출할 때 콜백함수의 매개변수로 속에서 꺼내서 쓰고 있음

--> 밖에서 가지고 오는 개념이 아니라 함수 안에서 가지고 와서 사용하는 개념

--> 1번보다 안정된 코드 !

 

function setTodo(updatedTodo: Todo) {
    // const [todos, setTodos] = useState<Todos>([]) 여기에서의 todos임
    setTodos(todos => {
      return todos.map(todo => {
        if (todo.id === updatedTodo.id) {
          return updatedTodo
        }
        return todo
      })
    })
  }

 

return을 왜 써야하는가 ?!

return을 사용하지 않으면 setTodos는 아무것도 받지 못합니다. 즉, 상태를 업데이트할 수 없습니다.
return을 사용하여 map() 함수가 반환한 새로운 배열을 setTodos에게 전달해야 합니다.
즉 return은 “새로운 상태를 React에게 전달하는 방법”입니다. 이를 통해 React가 상태를 올바르게 업데이트할 수 있습니다.

개념 살펴보기
React에서 상태를 업데이트할 때, setState 함수에 새로운 상태를 전달합니다. 상태는 보통 배열이나 객체와 같은 복잡한 데이터 구조입니다. 우리는 상태를 변경할 때, 변경된 새로운 데이터를 React에게 전달해줘야 합니다.


1. 현재 상태를 받아오기:
setTodos 함수에 콜백 함수(함수 안의 함수)를 전달합니다. 이 콜백 함수는 현재 상태인 todos 배열을 인수로 받습니다.
todos => {
  // 이 안에서 todos는 현재 상태입니다.
}​

 

2. 상태 변경:

todos.map()을 사용하여 현재 상태인 todos 배열의 각 항목을 순회합니다. 각 항목이 updatedTodoid와 일치하는지 확인합니다.

todos.map(todo => {
  if (todo.id === updatedTodo.id) {
    return updatedTodo;
  }
  return todo;
});​

 


3. 새 배열 반환:
map() 함수는 새로운 배열을 반환합니다. 이 새로운 배열은 변경된 상태를 반영합니다. 이 새로운 배열을 return하여 setTodos에게 전달해야 합니다.
return todos.map(todo => {
  if (todo.id === updatedTodo.id) {
    return updatedTodo;
  }
  return todo;
});​

--> return을 사용하지 않으면 map() 함수는 새로운 배열을 반환하지만, setTodos는 이 새로운 배열을 받지 못합니다. 그래서 상태가 업데이트되지 않습니다.

 

완성코드

--------------------------------App.tsx--------------------------------
function setTodo(updatedTodo: Todo) {
  // const [todos, setTodos] = useState<Todos>([]) 여기에서의 todos임
  setTodos(todos => {
    return todos.map(todo => {
      if (todo.id === updatedTodo.id) {
        return updatedTodo
      }
      return todo
    })
  })
}

;<TodoItem
  todo={todo}
  setTodo={setTodo}
/>

export default function TodoItem({
  todo,
  setTodo
}: {
  todo: Todo
  setTodo: (updatedtodo: Todo) => void
})

--------------------------------TodoItem.tsx--------------------------------
async function updateTodo() {
  console.log('서버로 전송', title)
  const res = await fetch(
    `https://asia-northeast3-heropy-api.cloudfunctions.net/api/todos/${todo.id}`,
    {
      method: 'PUT',
      headers: {
        'content-type': 'application/json',
        apikey: '5X8Z1k7M2vU5Q',
        username: 'Grepp_KDT4_ParkYoungWoong'
      },
      body: JSON.stringify({
        title,
        done: todo.done
      })
    }
  )
  const updatedTodo: Todo = await res.json()
  console.log(updatedTodo, title)
  setTodo(updatedTodo)
}

엔터 누른 뒤 시간이 지나고 ttile이 바뀜

 

2-2) 낙관적 업데이트
미리 수정 전송했던 그 데이터로 화면을 갱신
--> 데이터를 받으려면 시간이 걸리니까 일단 먼저 갱신('어차피 성공하겄지' 성공할 것을 예상하고)
--> 실패한 경우에는 미리 갱신한 것 다시 되돌리는 방법도 있음


- 장점: 사용자가 수정을 시도하는 순간 화면이 갱신되니까 속도가 굉장히 빠르다고 느낄 수 있음
cf. 낙관적 업데이트란 ?

낙관적 업데이트(Optimistic UI Update)는 사용자 경험을 향상시키기 위한 전략으로, 서버에서 실제 데이터가 업데이트되기 전에 UI를 먼저 업데이트하여 사용자에게 더 빠른 반응을 제공하는 방법입니다. 이 방법은 서버 요청이 성공할 것이라고 가정하고, 서버 요청이 실패할 경우에만 UI를 다시 롤백합니다.

 

엔터를 누르자 마자 title 곧바로 변경

 

cf. 낙관적 업데이트 이후 코드에서 에러가 나는 상황 처리

async function updateTodo() {
  // 낙관적 업데이트: UI를 먼저 업데이트
  setTodo({
    ...todo,
    title: title // title은 수정된 title로 바로 적용
  })

  console.log('서버로 전송', title)

  try {
    // 서버에 PUT 요청
    const res = await fetch(
      `https://asia-northeast3-heropy-api.cloudfunctions.net/api/todos/${todo.id}`,
      {
        method: 'PUT',
        headers: {
          'content-type': 'application/json',
          apikey: '5X8Z1k7M2vU5Q',
          username: 'Grepp_KDT4_ParkYoungWoong'
        },
        body: JSON.stringify({
          title,  // 수정된 title
          done: todo.done
        })
      }
    )
    
    // 서버 응답을 받아 업데이트된 todo를 얻음
    const updatedTodo: Todo = await res.json()
    console.log(updatedTodo, title)

    // 이미 낙관적 업데이트로 상태를 업데이트했기 때문에, 이 부분은 선택 사항
    // setTodo(updatedTodo)
  } catch (error) {
    console.error('서버 요청 실패:', error)
    // 어 ?! 문제가 생겼네 ..
    // 근데 나는 낙관적으로 업데이트를 이미 했는데 ..
    // 어쩌지 ?
    // 서버 요청 실패 시 롤백하는 로직을 추가할 수 있음 !!
    setTodo(todo)
  }
}

 

fetch 사용과 에러처리

fetch API는 HTTP 요청이 실패해도 기본적으로 네트워크 에러(예: 인터넷 연결 문제)만 catch 블록으로 전달합니다. HTTP 상태 코드가 4xx나 5xx인 경우는 에러로 간주되지 않으며, 이 경우에는 직접 처리해야 합니다.

if (!res.ok) {
  throw new Error(`HTTP error! status: ${res.status}`);
}​

--> fetch 응답의 ok 속성을 검사하여 HTTP 상태 코드가 성공적이지 않을 경우 에러를 던집니다. 이렇게 하면 네트워크 에러 뿐만 아니라 HTTP 에러도 catch 블록에서 처리할 수 있습니다.
async function updateTodo() {
  // 낙관적 업데이트: UI를 먼저 업데이트
  setTodo({
    ...todo,
    title: title // title은 수정된 title로 바로 적용
  })
  console.log('서버로 전송', title)

  try {
    const res = await fetch(
      `https://asia-northeast3-heropy-api.cloudfunctions.net/api/todos/${todo.id}`,
      {
        method: 'PUT',
        headers: {
          'content-type': 'application/json',
          apikey: '5X8Z1k7M2vU5Q',
          username: 'Grepp_KDT4_ParkYoungWoong'
        },
        body: JSON.stringify({
          title,  // 수정된 title
          done: todo.done
        })
      }
    )
    // HTTP 응답 상태 코드가 성공적인지 확인
    if (!res.ok) {
      throw new Error(`HTTP error! status: ${res.status}`);
    }
    
    const updatedTodo: Todo = await res.json()
    console.log(updatedTodo, title)

  } catch (error) {
    console.error('서버 요청 실패:', error)
    setTodo(todo)
  }
}​

 

 

두 번째 방법
1) 수정할 데이터를 서버로 전송 (성공)
2) 새로운 목록 자체를 서버에서 다시 가져옴 (입력값으로 수정된 목록)
- 장점: 작성하기 편리함
- 단점: 네트워크 통신을 두 번 해야 함 (성능 최적화 측면에서는 조금 아쉽)
--> 트레이드 오프 관계

 

updatetodo() 로 목록 업데이트 하고 나서 getTodo()로 목록을 가져오기

import/export 키워드는 하나의 파일의 최상위 레벨에 있어야 사용 가능 !

App 밖으로 빼서 최상위 레벨에 위치하게 만들어줘야 함

 

밖으로 빼지 않는 방법은 없을까 ?

tsx 파일은 파일이자 리액트 컴포넌트 ! 리액트 컴포넌트는 import/export 말고도 데이터 전달이 가능한 상황이 있음

--> getTodos()를 TodoItem에 컴포넌트로 직접 전달

--> props 형식으로 전달 (함수도 참조형 데이터 중 하나이기 때문에 전달 가능)

<TodoItem todo={todo} getTodos={getTodos}/>

TodoItem.tsx

 

--> 네트워크 통신이 2번 되는 모습

 

 

데이터가 삭제 되었을 때 화면 갱신 실습

두 번째 방법
1) 수정할 데이터를 서버로 전송 (성공)
2) 새로운 목록 자체를 서버에서 다시 가져옴 (입력값으로 수정된 목록)
- 장점: 작성하기 편리함
- 단점: 네트워크 통신을 두 번 해야 함 (성능 최적화 측면에서는 조금 아쉽)
--> 트레이드 오프 관계

deleteTodo로 todoItem을 지운 후에 getTodo로 목록을 다시 가져와라 !

 

 

--> trade off ! 코드는 간단하지만 네트워크 통신이 두 번되고 있음 !

 

 

 

첫 번째 방법
1) 수정할 데이터를 서버로 전송 (성공)
--> 수정한 데이터의 결과를 응답해주는데

2-1) 응답 받은 결과로 화면을 갱신 (수정된 부분만 갈아끼우는 것 !)
filter 메소드 설명

1) id가 'c'인 경우 false를 반환하니까 결국 세 번째 객체(true)는 반환하지 않음
2) id가 c랑 같지 않을 경우 반환

map과 filter


 

전체 코드

 

프로젝트 버전관리 시작하기

git

'--' --> flag

git config --global --list

--list: 현재 설정된 Git 설정 목록을 표시

 

force flag

웬만하면 쓰지 않기(코드를 다 덮어써버리기 때문)

어떤 결과가 나올 것인지를 내가 명백하게 알고 있을 때 사용하기

git push origin main --force
// 축약형: -f

 

상태관리를 위한 git 명령

git init (초기화)
git status

git add . (스테이징)
git status

git commit -m "커밋 메세지"

 

 

레파지토리 생성 시 꼭 필요한 경우 아니면 README 파일 안 만드는게 좋음 (충돌방지)

git remote add origin 'https://github.com/suhyun9892/beginner-react-todo.git'
--> origin: 내가 원하는 이름으로 써도 상관없지만, 통상 origin이라고 씀
git push origin main

cf. 하나의 프로젝트에 여러 저장소를 연결하고 싶다면 ? 별칭을 다르게 !
git remote add bitbucket '다른 주소 복사한 것'
git push bitbucket main

 

 

브랜치 생성 방법

git switch (git checkout보다 최신 명령)
git switch -h (사용법 띄우기)

git branch gettodos // getTodos 브랜치 만들게
git switch getTodos // getTodos 브랜치로 이동
--> git switch -c getTodos // getTodos 만들면서 이동까지 (= git checkout -b getTodos)

 노란 부분 = 버전의 해시값
checkout은 해시값을 가지고도 이동할 수 있는 기능이 포함 --> switch는 브랜치 이동만 !

브랜치 생성 시 --> git flow / github flow

 

 

API 명세서 일부

// 응답 데이터 타입 및 예시:

interface ResponseValue {
  id: string
  order: number
  title: string
  done: boolean
  createdAt: string
  updatedAt: string
}

{
  "id": "7P8dOM4voAv8a8cfoeKZ",
  "order": 0,
  "title": "KDT 과정 설계 미팅",
  "done": false,
  "createdAt": "2021-10-29T07:20:02.749Z",
  "updatedAt": "2021-10-29T07:20:02.749Z"
}
// 할일 객체 구조

 


* 파일과 파일 간 데이터를 주고받으려면 모듈화가 필요

* API 명세서는 꼭 보기 !

* stackoverflow 등 .. 잘못된 내용이 많으니 원하는 정보에 대한 힌트를 얻고 다시 구글링
이런 식으로 문제해결 해보기 (gpt가 만능인 것 처럼 쓰지 말자)

* TS에서 any는 되도록 피하기
cf. unknown (어떤 타입인지 잘 모르겠어요, any의 대체재)

* fetch 에서는 에러 나도 catch로 가지 않음(따로 설정)

 

 

https://www.heropy.dev/p/PcUkdT

 

Git 핵심 명령어 모음

버전 관리 시스템(VCS) Git에서 주로 사용하는 명령을 빠르게 정리합니다.

www.heropy.dev

 

반응형