connecting dots

Beginner 3회차 (7/25) | Redux 알아보기, HTTP 상태코드, 동기와 비동기 이해, 모듈(import/export), 비동기로 데이터 가지고 오기(promise, async/await, fetch), useState와 useEffect 본문

Live Class/Beginner

Beginner 3회차 (7/25) | Redux 알아보기, HTTP 상태코드, 동기와 비동기 이해, 모듈(import/export), 비동기로 데이터 가지고 오기(promise, async/await, fetch), useState와 useEffect

dearsuhyun 2024. 7. 25. 17:39

Redux 알아보기

- state: 데이터

- action: 함수

함수도 하나의 데이터이긴 함. 데이터 중 함수는 호출 가능

호출을 해서 추가적인 명령을 실행하는 개념

 

동기 처리는
1) 액션을 만들어서 데이터를 어떻게 변경할지 type(액션의 이름)과 payload(매개변수)로 설정한다.
2) 필요한 곳에서 디스패치 액션을 호출한다.
3) 액션(내부 로직으로)에서 상태를 업데이트할 수 있다.

비동기 처리는
1) 액션을 만들어서 데이터를 어떻게 변경할지 type(액션의 이름)과 payload(매개변수)로 설정한다.
2) 필요한 곳에서 디스패치 액션을 호출한다.
3) 액션(내부 로직으로)에서 상태를 업데이트할 수 있다.
- fulfilled/rejected (무엇을 의미하는 것인지 등은 파악해야 함)

만일 사용자가 버튼을 클릭하는 등의 이벤트가 발생하면 디스패치가 액션을 사용하는 것이다.
리듀서가 액션을 받아서 새로운 상태를 만들고 스토어가 상태를 업데이트 한다. (액션이 가진 기본 로직)

 

 

Redux란 ?

copilot said ...
Redux는 JavaScript 애플리케이션에서 상태 관리를 쉽게 할 수 있도록 도와주는 라이브러리입니다.

핵심 개념
1. 상태(state): 애플리케이션의 현재 상태를 나타내는 객체입니다.
==>
도서관에서 책들이 있는 선반을 생각하세요. 이 선반은 현재 도서관의 상태를 나타내요. 예를 들어, 어떤 책들이 어디에 위치하고 있는지, 어떤 책들이 대출 중인지 등을 알 수 있어요.

2. 액션(action): 상태를 변경하기 위한 지시사항입니다. 액션은 무엇이 일어났는지 설명하는 객체입니다.
==>
도서관에서 책을 빌리거나 반납하려고 할 때, 대출/반납 신청서를 작성해요. 이 신청서가 바로 액션이에요. 신청서에는 ‘책을 빌리기’ 또는 ‘책을 반납하기’라는 명령(type)과 어떤 책인지 정보(payload)가 들어있어요.

3. 리듀서(reducer): 액션에 따라 상태를 어떻게 변경할지 정의하는 함수입니다. 리듀서는 이전 상태와 액션을 받아 새로운 상태를 반환합니다.
==>
도서관 사서가 신청서를 보고 책을 빌려주거나 반납해주는 역할을 해요. 리듀서는 사서처럼, 현재 상태(선반에 있는 책들)와 신청서(액션)를 보고 새로운 상태(업데이트된 선반 상태)를 만들어줘요.

4. 스토어(store): 상태를 보관하는 객체입니다. 애플리케이션의 상태, 액션 및 리듀서를 연결하는 역할을 합니다.
==>
도서관 전체 시스템을 관리하는 중앙 사무실을 생각하세요. 이 사무실이 바로 스토어에요. 스토어는 도서관의 현재 상태(선반 상태)를 저장하고, 새로운 신청서(액션)를 처리해서 상태를 업데이트해요.

이렇게 Redux는 도서관처럼 상태를 체계적으로 관리하고, 변경사항을 예측 가능하게 만들어줘요.

 

1. 액션 정의: 책을 빌리고 반납하는 액션을 정의해요.

const borrowBook = {
  type: 'BORROW_BOOK',
  payload: { title: 'Harry Potter' }
};

const returnBook = {
  type: 'RETURN_BOOK',
  payload: { title: 'Harry Potter' }
};

 

2. 리듀서 정의: 사서가 신청서를 보고 책을 빌려주거나 반납하는 함수에요.

function libraryReducer(state = [], action) {
  switch (action.type) {
    case 'BORROW_BOOK':
      return state.filter(book => book.title !== action.payload.title);
    case 'RETURN_BOOK':
      return [...state, action.payload];
    default:
      return state;
  }
}
함수 설명


1. 초기 상태 설정:
state = []: 초기 상태로 빈 배열을 사용합니다. 도서관의 책 목록이 빈 상태로 시작합니다.
2. 액션 타입에 따른 상태 변경:
action 객체는 typepayload를 가집니다. type은 어떤 종류의 액션인지 나타내고, payload는 액션과 함께 전달되는 데이터입니다.
3. switch 문:
switch 문은 action.type에 따라 다른 작업을 수행합니다.


액션 타입 처리


1. ‘BORROW_BOOK’:
이 액션은 책을 빌릴 때 사용됩니다.
state.filter(book => book.title !== action.payload.title): 현재 상태에서 책 제목이 action.payload.title과 일치하지 않는 책들만 남깁니다. 즉, 빌린 책을 책 목록에서 제거합니다.
2. ‘RETURN_BOOK’:
이 액션은 책을 반납할 때 사용됩니다.
[...state, action.payload]: 현재 상태에 action.payload(반납된 책)를 추가합니다. 즉, 책 목록에 반납된 책을 추가합니다.
3. 기본 동작 (default):
default: 액션 타입이 ‘BORROW_BOOK’ 또는 ‘RETURN_BOOK’이 아닌 경우 현재 상태를 그대로 반환합니다. 아무 변화도 없음을 의미합니다.

 

3. 스토어 생성: 도서관 중앙 사무실을 만들어요.

import { createStore } from 'redux';

const store = createStore(libraryReducer);

 

4. 상태 변경 및 조회: 책을 빌리고 반납하고, 현재 상태를 확인해요.

// 현재 도서관 상태 (선반)
console.log(store.getState()); // 초기 상태는 빈 배열 []입니다. 즉, 도서관에는 현재 책이 없습니다.
// 책을 반납해서 선반에 추가
store.dispatch(returnBook);
console.log(store.getState()); // [{ title: 'Harry Potter' }]

store.dispatch(returnBook)을 호출하여 returnBook 액션을 디스패치합니다.

returnBook 액션은 { type: 'RETURN_BOOK', payload: { title: 'Harry Potter' } }입니다.

libraryReducer가 이 액션을 처리하여 Harry Potter 책을 도서관 상태에 추가합니다.

store.getState()를 다시 호출하면, 상태는 [{ title: 'Harry Potter' }]이 됩니다. 즉, 도서관 선반에 Harry Potter 책이 추가되었습니다.

 

cf. dispatch()

dispatch는 Redux에서 중요한 개념 중 하나로, 액션을 스토어에 전달하여 상태를 변경하는 역할을 합니다.

쉽게 말해, dispatch는 “이런 일이 일어났어!“라고 Redux 스토어에 알려주는 방법입니다.

스토어는 전달받은 액션을 리듀서로 보내고, 리듀서는 새로운 상태를 계산하여 스토어에 반환합니다.

// 책을 빌려서 선반에서 제거
store.dispatch(borrowBook);
console.log(store.getState()); // []

store.dispatch(borrowBook)을 호출하여 borrowBook 액션을 디스패치합니다.

borrowBook 액션은 { type: 'BORROW_BOOK', payload: { title: 'Harry Potter' } }입니다.

libraryReducer가 이 액션을 처리하여 Harry Potter 책을 도서관 상태에서 제거합니다.

store.getState()를 다시 호출하면, 상태는 []이 됩니다. 즉, 도서관 선반에서 Harry Potter 책이 제거되었습니다.

 

HTTP 상태코드

페이지 못 찾음 = 404 에러: 페이지 띄우는 html 못 찾음

서버 오류 = 5**(대표적으로 500, 응답을 못하겠다는 응답을 보냄)

변경사항 = 3**

정상 = 2**

https://hongong.hanbit.co.kr/http-%EC%83%81%ED%83%9C-%EC%BD%94%EB%93%9C-%ED%91%9C-1xx-5xx-%EC%A0%84%EC%B2%B4-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC/

 

[웹 프로그래밍] HTTP 상태 코드 표(100 ~ 500) 전체 요약 정리

서버에서의 처리 결과는 응답 메시지의 상태 라인에 있는 상태 코드(status code)를 보고 파악할 수 있습니다. 상태 코드는 세 자리 숫자로 되어 있는데 첫 번째 숫자는 HTTP 응답의 종류를 구분하는

hongong.hanbit.co.kr

모듈이란?

import 혹은 export 중 최소한 하나의 키워드가 쓰여져 있는 코드 파일

export (내보내기)

- 기본 내보내기(default export): 딱 하나만 내보낼 수 있음, 이름이 없음

- 이름 내보내기(named export): 개수 제한 없음, 이름 필수

- {}의 경우 객체 데이터를 의미하는게 아니라 그냥 export 문법임 

 

 

export {} 형식 말고 각각 export 하는 형식도 가능

 

TS에서는 타입변수도 내보내기 가능

 

cf. interface란 ?

copilot said ...
interface는 프로그래밍에서 특정 객체가 가져야 하는 속성과 메서드를 정의하는 데 사용됩니다. 주로 TypeScript와 같은 정적 타입 언어에서 사용됩니다. interface를 사용하면 객체의 구조를 명확하게 정의할 수 있어, 코드의 가독성과 유지보수성을 높일 수 있습니다. TypeScript에서 객체가 특정 구조를 따르도록 강제할 때 사용됩니다.

프로그래밍에서 인터페이스는 객체가 가져야 할 속성과 메서드를 정의하는 설계도와 같습니다. 이를 통해 객체가 어떤 구조를 가져야 하는지 명확하게 알 수 있습니다.
// 인터페이스 정의
interface Book {
  title: string;
  author: string;
  pages: number;
  read(): void;
}

// 객체 생성
const myBook: Book = {
  title: 'Harry Potter',
  author: 'J.K. Rowling',
  pages: 500,
  read() {
    console.log(`Reading ${this.title} by ${this.author}`);
  }
};

// 인터페이스를 구현하는 함수
function printBookInfo(book: Book) {
  console.log(`Title: ${book.title}, Author: ${book.author}, Pages: ${book.pages}`);
}

myBook.read(); // Reading Harry Potter by J.K. Rowling
printBookInfo(myBook); // Title: Harry Potter, Author: J.K. Rowling, Pages: 500

 

 

import(가져오기)

import abc from "./app";

            → default로 내보내진 것을 가져올 때는 이름 내 마음대로 지정 가능 

                이름 가져오기 같은 경우 {} 으로 묶어주어야 하고, export한 이름과 같은 이름을 사용

                이름을 이 파일에서 바꿔 쓰고 싶다면 {count as c, double as d} 이런 식으로 'as' 사용해서 지정

경로같은 경우 타입스크립트는 '.ts'를 생략해준다 (나중에 js로 변환 시 이슈 대비)

 

 

type 가져올 때 방법 3가지

 

비동기 데이터 가져오기

비동기 처리

                                        [데이터기다리기]

                                        [다음작업하면서

클라이언트(브라우저) -(요청: 데이터 주세요)-> 서버

                                     <네트워크 통신 발생>

                                   <-(응답: 알았어 줄게)-

 

네트워크 통신은 걸리는 시간을 알 수 없음. 기다려야 함

무작정 기다리기만 하면 브라우저가 멈춰버림.

요청을 보내놓으면 일단은 기다리되, 기다리는 동안 다른 작업을 하라고 처리해주는 방식

==> 비동기 처리 !!

promise / async-await / fetch

 

동기처리 (순서대로)

                                 [데이터올때까지기다리기]

클라이언트(브라우저) -(요청: 데이터 주세요)-> 서버

                                     <네트워크 통신 발생>

                                   <-(응답: 알았어 줄게)-

 

실행 순서가 있음. 받은 다음에 다음 작업을 처리하는 방식

 

https://www.heropy.dev/p/5PlGxB

 

사용자 정보 API

가짜 사용자 정보(Mock Data)가 필요할 때 사용할 수 있는 REST API를 제공합니다. JSON 형식으로 사용자 정보를 조회, 생성, 수정, 삭제(CRUD / Create, Read, Update, Delete)할 수 있습니다.

www.heropy.dev

 

promise / async await / fetch 사용 예시 단계별로 살펴보기

curl https://api.heropy.dev/v0/users
  \ -X 'GET'
copilot said ...
curl 명령어란 ?
curl 명령어는 터미널에서 HTTP 요청을 보내고 응답을 받는 데 사용되는 도구입니다. 주로 API 호출을 테스트하거나 웹 페이지를 가져오는 데 사용됩니다. 

이 명령어는 다음과 같은 작업을 수행합니다:
1. curl: 터미널에서 HTTP 요청을 보내기 위해 사용하는 도구입니다.
2. https://api.heropy.dev/v0/users: 요청을 보낼 API 엔드포인트 URL입니다.
3. -X 'GET': HTTP 메서드를 지정합니다. 여기서는 GET 요청을 보냅니다. (참고: GET 요청은 기본 메서드이므로 -X 'GET'을 생략해도 됩니다.)

쉽게 설명
1. curl: 인터넷에 요청을 보내는 프로그램.
2. https://api.heropy.dev/v0/users: 요청을 보낼 주소.
3. -X 'GET': 데이터를 가져오기 위한 요청 방법을 지정 (여기서는 GET 요청).

 

// 네트워크 통신을 위한 함수
// Promise 객체: fetch 함수가 어떤 데이터를 가져왔는지에 대한 약속(데이터는 아님)
// responseObject(res)안에 데이터 포함(그 자체가 데이터라는 게 아님)

const res = fetch('https://api.heropy.dev/v0/users') // 요청만 함

console.log(res) // promise {< pending >}
console.log('서버에서 가져온 데이터') // 내용 없음

요청만 했기 때문에 받아오는 것에 대해서는 ???? 상태

 

const responseObject = await fetch('https://api.heropy.dev/v0/users') // 데이터 오는거 까지 기다려 주세요
await responseObject.json() // 데이터 꺼내는 거 까지 기다릴게요 (택배 뜯는 것 --> 여가까지 해야 데이터가 나옴)

console.log('서버에서 가져온 데이터') // 데이터 출력됨

요청하고 데이터 오는 것 까지 기다릴게요, 데이터를 꺼내는 것 까지도 기다릴게요

--> await 사용으로 데이터 받는 것까지 !

but await는 단독으로 사용이 불가능함

 

async function () {
  const responseObject = await fetch('https://api.heropy.dev/v0/users')
await responseObject.json() 

console.log(data)
}

await 에서 가장 가까운 함수에 async를 걸어줘야 함

but 지금 함수를 보면 익명함수 ! 익명함수는 호출(call)할 수 없음

 

async function abc() { // 함수를 정의한 것(만든 것, 이것 만으로는 동작하지 않음)
  const responseObject = await fetch('https://api.heropy.dev/v0/users')
await responseObject.json() 

console.log(data)
}
abc()
// 사용. 함수는 실행해야 동작하는 것임.

함수에 abc라는 이름을 줘서 abc()라고 호출 가능

함수는 정의하는 것만으로는 동작하지 않고 호출을 해줘야 함

 

user 목록 화면에 띄우기 예제

전체코드

 

 

1. 라이브러리와 타입 정의

import { useState, useEffect } from 'react'

 

useState: react hook, 상태를 추가하는 데 사용됨

useEffect: react hook, side effect를 처리하는 데 사용됨 (컴포넌트가 처음 렌더링될 때 데이터를 가져오는 작업)

 

cf. 사이드 이펙트(side effect)

함수가 자신의 범위를 벗어나 외부 상태를 변경하거나, 외부 시스템과 상호작용하는 것

즉, 함수가 직접적으로 자신과 관련된 작업 외에 다른 일을 하는 경우

 

export interface Root {
  total: number
  users: User[]
}

export interface User {
  id: string
  name: string
  age: number
  isValid: boolean
  emails: string[]
  photo?: Photo
}

export interface Photo {
  name: string
  size: number
  mimeType: string
  url: string
}

 

Root: 전체 응답의 타입을 정의. total은 사용자 수, usersUser 객체 배열.

User: 사용자 객체의 타입을 정의. 각 사용자는 id, name, age, isValid, emails를 가지고 있고, photo는 선택적.

Photo: 사진 객체의 타입을 정의. name, size, mimeType, url을 포함.

 

2. 컴포넌트 정의

- 상태 초기화

export default function App() {
  const [users, setUsers] = useState<User[]>([])
  const [count, setCount] = useState(31)

 

  const [users, setUsers] = useState([])
  // 여기에서 빈 배열에는 무엇이 들어가 있을지 알 수가 없음
  // 즉 이 배열데이터가 어떤 타입인지 모르는 상태
  // ts는 쌩으로 빈 배열이라고 판단하고 이 안에는 아무것도 들어갈 수 없다고 판단함
  // never[]
  // 이렇게만 해주면 밑에 jsx 문법 부분에서 {user.name}에 오류 발생
  // user는 never이기 때문에 name이라는 속성이 존재 x
  
  const [users, setUsers] = useState<User[]>([])
  // 따라서 Users의 배열이라고 type을 직접 명시해줘야 함

const [users, setUsers] = useState([]) 라고 해준 경우 발생하는 오류

 

  const [count, setCount] = useState(31)
  // 이 부분의 경우는 초기 값이 이미 number이기 때문에 type 추론 가능, 따로 명시 안해줘도 됨

 

- 카운터 증가 함수

  function increase() {
    setCount(count + 1)
  }

 

- 새로운 데이터를 가져오는 함수

  useEffect(() => {
    getUsers() 
  }, [])
  
  async function getUsers() {
    const res = await fetch('https://api.heropy.dev/v0/users')
    const data = await res.json()
    console.log('응답결과: ', data)
    setUsers(data.users)
  }

 

useEffect: 컴포넌트가 처음 렌더링될 때 getUsers 함수를 호출하여 데이터를 가져옴

getUsers: API 요청을 보내 사용자 데이터를 가져오는 비동기 함수. 데이터를 받아와서 users 상태를 업데이트함

 

동작 순서

1. 컴포넌트가 처음 렌더링될 때:

useEffect가 실행됨.

빈 배열 []이 전달되었기 때문에, 이 효과는 컴포넌트가 처음 마운트될 때 한 번만 실행됨.

 

2. getUsers 함수 호출:

getUsers 함수가 호출되어 API 요청을 보냄.

응답을 JSON으로 변환한 후, setUsers를 호출하여 users 상태를 업데이트함.

이렇게 하면 컴포넌트가 다시 렌더링되어 사용자 목록이 화면에 표시됨.

 

  // App 컴포넌트가 준비되었을 때(Mount, 최초 렌더링) 함수 요청하면 됨
  // useEffect(콜백, 의존성배열);
  // app 컴포넌트가 준비되면 getUsers라는 콜백함수를 호출하겠다는 뜻
  
  useEffect(() => {
    getUsers() 
  }, [])

 

useEffect는 컴포넌트가 렌더링될 때 실행됨

첫 번째 인수로 콜백 함수가 전달돰. 이 콜백 함수는 사이드 이펙트를 처리하는 데 사용됨.

두 번째 인수로 빈 배열 []이 전달됨. 이는 useEffect가 처음 컴포넌트가 마운트될 때 한 번만 실행된다는 것을 의미함.

 

✅  함수를 선언하기 전에 호출했는데 정상적으로 작동하는 이유 ?!

--> 호이스팅(hoisting) 때문 !!

 

호이스팅(Hoisting)이란 ?

JavaScript의 동작 방식 중 하나로, 변수와 함수 선언이 실제 코드가 실행되기 전에 위로 끌어올려지는 것처럼 동작하는 현상

코드의 선언부가 마치 코드의 맨 위로 이동한 것처럼 동작함

// 함수 표현식 (할당 연산자가 있는 것)
// 호이스팅 되지 않음.
const getUsers = async () => {}

// 함수 선언식
// 호이스팅 가능. 위로 올라가서 정의되기 때문에 정상적으로 동작
async function getUsers() {}

 

  async function getUsers() {
    const res = await fetch('https://api.heropy.dev/v0/users')
    const data = await res.json()
    console.log('응답결과: ', data)
    setUsers(data.users)
  }

 

fetch를 사용하여 https://api.heropy.dev/v0/users에서 데이터를 가져옴

가져온 데이터를 JSON으로 변환한 후 콘솔에 출력하고, setUsers를 사용하여 users 상태를 업데이트함

상태가 업데이트되면 컴포넌트가 다시 렌더링되어 사용자 목록이 화면에 표시됨 !

 

async는 await키워드를 선언한 곳에서 가장 가까운 함수에 붙여줌

res = response(응답) or result(결과) 둘 중 하나의 의미로 사용됨

 

--> 데이터를 가지고 오는 것을 기다리면서 다른 걸 하나보면 res라는 택배가 옵니다 (res 안에는 데이터가 담겨 있음)

택배를 뜯는 것도 기다리면서(택배 크기에 따라 속도가 다르고 이는 예측 불가하기 때문) 다른 걸 하다보면 데이터를 얻을 수 있습니다 !

 

3. 컴포넌트 렌더링

  return (
    <>
      <div>{count}명</div>
      <button onClick={increase}>증가+</button>
      <ul>
        {users.map(user => (
          <li key={user.name}>{user.name}</li>
        ))}
      </ul>
    </>
  )
}

 

<ul>: 사용자 목록을 출력. 각 사용자 이름을 리스트 항목으로 렌더링함

 

성공적으로 화면에 띄우기 성공 !

 

 

표기법

hello_world: 뱀 케이스(snake_case)
// html, css, js 다 씀. js에서는 속성의 이름을 다룰 때 쓸 수 있음

hello-world: 케밥 케이스(kebab-case)
// JS에서는 안 씀

helloWorld: 카멜 케이스
// JS의 대부분의 데이터 이름 만들 때

HelloWorld: 파스칼 케이스(PascalCase)
//JS클래스 만들 때 사용함(컨벤션)

 

함수 호출

함수 실행되면 결과물을 남기고 사라진다

 

 

* 다른 사람이 올리는 질문에 관심 가지기
모르는 부분이든 아는 부분이든 어떻게 답변이 달리는지 보고
추가질문도 생긴다면 적극적으로 해보기

* 실무에서 리덕스 쓰는 경우
예전에 만드는 프로젝트 유지보수 하는 경우
새로운 프로젝트에는 거의 도입되지 않는 편 

* promise 객체가 어떻게 만들어지는지 아는 것은 중요함 (쉬운 내용은 아님)
* then()은 잘 안쓰이고 async/await 형식을 많이 씀
* 함수 이름은 명령형으로 작명하기(getUsers - 동사가 먼저 나옴/ 사용자를 가져와)
* 변수 이름은 명사로 작명하기 (users - 명사 / 사용자)
* 프론트엔드 개발에서 비동기로 데이터 다루는 것은 필수 !

* request/response하는 과정 중간은 원격(remote)이다 !
데이터 베이스에서 데이터를 꺼낼 수 있는 백엔드 서버는 내 컴퓨터에 없음 --> 어떻게 가지고 와 ?
인터넷을 이용해서 네트워크로 통신을 하는 것임 (대표적인 비동기)
- 데이터 요청하는 코드 필요
- 네트워크 에러(인터넷 끊기는 것)로 데이터가 못 왔을 때, 내 컴퓨터에서는 왜 안오지 ? 일정시간 지난 후에 에러를 띄움.
- 요청이 거부(reject)되었을 때

 

 

json --> typescript

https://transform.tools/json-to-typescript

 

JSON to TypeScript

to TypeScript Declaration to TypeScript Declaration

transform.tools

 

반응형