connecting dots

React & TS 5회차 (7/22) | todo 리스트 만들기 실습, prop drilling, Flux 아키텍쳐, Redux, Storybook 본문

Live Class/DevCamp

React & TS 5회차 (7/22) | todo 리스트 만들기 실습, prop drilling, Flux 아키텍쳐, Redux, Storybook

dearsuhyun 2024. 7. 31. 09:47

todo 리스트 만들기 실습

input에 할 일 써서 추가버튼 누르면 리스트에 할 일 추가

import { useState } from "react";

function App() {
  const [todos, setTodos] = useState([]);
  const [todoText, setTodoText] = useState("");
}

const onChangeTodoText = (e) => {
  setTodoText(e.target.value);
  // target은 실제로 이벤트가 발생한 요소 = input 태그
  // input type="text"는 value 속성을 가지고 있어서 value 속성을 통해 입력된 값을 가져올 수 있음
};

const onAddTodo = () => {
  // 새로운 배열 만들고 원소들을 다 펼쳐서 다시 집어넣어 + 새롭게 추가할 원소를 추가
  // 객체에서도 사용 가능
  setTodos([...todos], {
    id: todo.length + 1,
    title: todoText,
    done: false
  });
  setTodoText(""); // 추가하면 인풋 부분 비워주는 역할
};

return (
  <div>
    <h1>context & Flux 아키텍쳐</h1>

    <div>
      <label>할 일: </label>
      <input type="text" value={todoText} onChange={onChangeTodoText} />
      <button onChange={onAddTodo}>추가</button>
    </div>

    <div>
      <ul>
        {
          // 배열 안의 값을 다른 값으로 바꿔줄 때는 항상 map 사용
          todos.map((todo, index) => (
            <li key={index}>
              <input type="checkbox" checked={todo.done} />
              <span>{todo.title}</span>
              <button>삭제</button>
            </li>
          ))
        }
      </ul>
    </div>
  </div>
);

 

 

삭제 기능 구현

const onDeleteTodo = (id) => {
  // filter = 배열을 순회함(배열을 한 번만 읽어서 무언가를 하고 끝내는 것)
  setTodos(todos.filter((todo) => todo.id !== id)); // id가 같지 않은 것만 남기고 나머지는 버림 (삭제효과)
};

 <button onClick={() => onDeleteTodo(todo.id)}>삭제</button>
{/* 이벤트 시스템은 이벤트 객체를 줌. 우리는 id를 가진 호출, onDeleteTodo를 함수로 감싸줘야 함 */}

 

이벤트 핸들러와 함수 호출

1. 직접 호출

<button onClick={onDeleteTodo(todo.id)}>삭제</button>

 

이 코드는 onDeleteTodo(todo.id) 함수 호출의 반환값을 onClick 속성에 할당하는 것입니다.

함수 호출은 즉시 실행되며, 그 반환값이 onClick 속성에 할당됩니다.

예를 들어, onDeleteTodo(todo.id)undefined를 반환한다면, onClick={undefined}가 됩니다. 이는 이벤트 핸들러로써 작동하지 않습니다.

 

이 방식은 컴포넌트가 렌더링될 때 onDeleteTodo 함수가 즉시 실행됩니다.

즉, 버튼을 클릭하지 않아도 컴포넌트가 렌더링될 때마다 onDeleteTodo 함수가 호출됩니다.

이는 우리가 의도한 바가 아닙니다. 우리는 버튼을 클릭했을 때만 함수가 실행되기를 원합니다.

 

렌더링 시점에 함수 호출이 일어나는 이유

1. 즉시 호출: 함수 이름 뒤에 괄호 ()를 붙이면, 함수가 즉시 실행됩니다.
• 예: onDeleteTodo(todo.id)는 함수 호출입니다. 이 코드는 즉시 실행되고, 반환값이 onClick 속성에 할당됩니다.

2. 함수 정의 전달: 함수 이름 뒤에 괄호를 붙이지 않으면, 함수 그 자체를 전달합니다.
• 예: () => onDeleteTodo(todo.id)는 화살표 함수로, 새로운 함수를 정의합니다. 이 함수는 나중에 이벤트가 발생할 때 호출됩니다.

 

React에서 이벤트 핸들러 설정

기본구조
import React from 'react';

function App() {
  function handleClick() {
    alert('Button was clicked!');
  }

  return (
    <button onClick={handleClick}>Click me</button>
    // <button onClick={handleClick()}>Click me</button>
    // 이렇게 호출하면 즉시 호출 !
  );
}

export default App;

 


• 이벤트 핸들러로 함수 호출이 아닌 함수 정의를 전달해야 합니다. 즉, handleClick()이 아니라 handleClick을 전달합니다.
• 이렇게 하면 React는 이벤트가 발생할 때 전달된 함수를 호출합니다.

이벤트 핸들러에 매개변수 전달
이벤트 핸들러에 매개변수를 전달해야 하는 경우, 화살표 함수를 사용하여 함수 호출을 감싸는 방법을 사용합니다.
import React from 'react';

function App() {
  function handleClick(name) {
    alert(`Hello, ${name}!`);
  }

  return (
    <button onClick={() => handleClick('Alice')}>Click me</button>
  );
}

export default App;​

위 코드에서 handleClick('Alice')는 버튼이 클릭될 때만 호출됩니다. 화살표 함수 () => handleClick('Alice')가 onClick 속성에 전달됩니다.

 

2. 함수로 감싸기

<button onClick={() => onDeleteTodo(todo.id)}>삭제</button>

 

이 방식은 버튼을 클릭했을 때만 onDeleteTodo 함수가 실행됩니다.

따라서 onClick 속성에는 화살표 함수 자체가 할당됩니다. 이 함수는 버튼이 클릭될 때 실행되며, 그 안에서 onDeleteTodo(todo.id)를 호출합니다.

 

토글 버튼으로 체크하는 기능구현

const onToggleTodo = (id) => {
  if (todo.id === id) {
    return {
      ...todo,
      done: !todo.done,
      // 거짓 -> 참, 참 -> 거짓
    };
  } else {
    return todo;
  }
};

<button onClick={() => onDeleteTodo(todo.id)}>삭제</button>
// 이벤트 시스템은 이벤트 객체를 줌. 우리는 id를 가진 호출, onDeleteTodo를 함수로 감싸줘야 함

 

return {
  id: todo.id,
  title: todo.title,
  done: todo.done,
} 

return {...todo}

// 같은 코드 !어떠한 객체의 key를 기반으로 새로운 객체를 만들고 있음(복사하는 형식으로)
// 요소 key가 많아지면 번거롭기 때문에 전개 연산자 사용

 

컴포넌트화

// App.jsx

import {} from "./item";

<div>
      <ul>
        {
          // 배열 안의 값을 다른 값으로 바꿔줄 때는 항상 map 사용
          todos.map((todo, index) => (
            <Item
              key={index}
              id={todo.id}
              title={todo.title}
              done={todo.done}
              onDelete={onDeleteTodo}
              onDone={onToggleTodo}
            />
          ))
        }
      </ul>
    </div>
  </div>
export const Item = ({ id, title, done, onDelete, onDone }) => {
  // props로 받아온 값을 사용할 때는 {}로 감싸줌
  <li>
    <span>{id}</span>
    <input type="checkbox" checked={done} onChange={() => onDone(id)} />
    <span>{title}</span>
    <button onClick={() => onDelete(id)}>삭제</button>
  </li>;
};

 

--> 컴포넌트화를 하면 App.jsx에서의 코드가 간단해지며 재사용이 가능 ..

 

but ! 치명적인 단점 --> prop drilling

Prop Drilling은 리액트 애플리케이션에서 상위 컴포넌트의 데이터(혹은 함수)를 여러 중간 컴포넌트를 거쳐 하위 컴포넌트로 전달하는 과정을 의미합니다. 이는 중간 컴포넌트가 실제로는 그 데이터를 사용하지 않지만, 단지 데이터를 전달하기 위해 props를 받는 상황을 발생시킵니다.

출처: https://ko.react.dev/learn/passing-data-deeply-with-context

 

왜 Prop Drilling이 발생하나요?

 

리액트 컴포넌트는 트리 구조로 이루어져 있습니다. 특정 데이터가 트리의 최상위에 있는 컴포넌트에서 최하위 컴포넌트로 전달되어야 할 때, 모든 중간 컴포넌트는 그 데이터를 전달받아야 합니다. 이 과정이 Prop Drilling입니다.

 

Prop Drilling의 문제점

 

복잡성 증가: 많은 중간 컴포넌트를 거치게 되면 코드가 복잡해지고, 추적하기 어려워집니다.

유지보수 어려움: 중간 컴포넌트들이 많아질수록 props 전달 경로를 수정하는 것이 어렵고, 에러 발생 가능성이 높아집니다.

 

Prop Drilling의 해결책 --> Flux 아키텍쳐

 

학습 순서
1) flux 구조 학습

2) redux 기본 철학 학습
- 단일 스토어 정책: 앱이 아무리 커도 객체가 하나이고 필요한 데이터들을 key로 뽑아서 쓰는 형태로 이루어져 있음
- 불변 데이터: reducer가 data를 업데이트하는데, 업데이트 방식이 특이함
변수가 없고(=변하는 데이터가 없고) 상수만을 가지고 만듦 --> '데이터가 안 변한다면 ? 한번만 테스트 하면 되잖아' --> 함수형 프로그래밍
데이터는 불변 ! --> 데이터를 계속 새로 만드는 방식 --> UI는 늘 새로운 것을 구현하면 됨


3) react redux

4) 비동기 상태 관리 (ex. Redux Saga)

5) redux toolkit 및 기타 등등

 

Flux 아키텍쳐

UI를 만드는 데이터의 흐름을 한 방향으로 만들어내는 구조

Redux

Flux 아키텍쳐를 구현한 라이브러리

리액트 뿐만 아니라 js 등 다른 곳에서 쓰는 것도 가능

 

단일 스토어

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

 

상태 내의 무언가를 변경하기 위해서는 액션을 디스패치 해야함

액션(action)은 어떤 작업을 할지 설명해주는 자바스크립트 순수 객체

액션에 따라 어떤 리듀서를 연결해줄 것인지가 결정됨

컨벤션: 항상 type이라는 속성을 가지고 있어야 함

타입 문자열을 앱에서 유일해야 함 --> 잘못된 리듀서가 호출되지 않도록

 

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

 

 

리듀서는 그저 상태와 액션을 인자로 하고, 애플리케이션의 다음 단계를 리턴해주는 함수일 뿐

리듀서라는 함수에게 액션 객체를 전달해주면

function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map((todo, index) =>
        action.index === index
          ? { text: todo.text, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

 

--> 리듀서는 인자를 두 개받음

1) state(store 객체 전체 값)

2) action(전달된 액션 객체)

 

리듀서는 항상 무엇인가를 리턴함

바꾸기 직전의 현재 상태를 받고 내가 리턴한 걸로 바뀜 --> 다음 리듀서가 받음

내가 바꿀 현재 상태의 전체 데이터를 받아 내가 필요한 것을 바꿔서 새로 리턴하면 단일 스토어 전체가 내가 바꾼 걸로 다 업데이트 됨

동기적인 작업만 가능 --> redux saga(비동기 작업 처리해주는 대표적인 라이브러리)

 

Storybook

컴포넌트들을 문서처럼 만들 수있고 동작, 테스트 가능하고 prop 임의로 넣어 조작할 수 있음

--> 컴포넌트를 시각화하고 컴포넌트의 구현 코드를 문서화해줌

 

Next.js

 

 


https://velog.io/@rachel28/Prop-Drilling

 

[React] Prop Drilling 이란 무엇이고, 어떻게 해결할 수 있을까?

Props Drilling 이란 무엇이고, 어떻게 해결할 수 있을까?

velog.io

https://ko.react.dev/learn/managing-state

 

State 관리하기 – React

The library for web and native user interfaces

ko.react.dev

https://ko.redux.js.org/

 

Redux - 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너. | Redux

자바스크립트 앱을 위한 예측 가능한 상태 컨테이너.

ko.redux.js.org

https://redux-saga.js.org/

 

Redux-Saga - An intuitive Redux side effect manager. | Redux-Saga

An open source Redux middleware library for efficiently handling asynchronous side effects

redux-saga.js.org

https://storybook.js.org/tutorials/intro-to-storybook/react/ko/get-started/

 

Storybook Tutorials

Learn how to develop UIs with components and design systems. Our in-depth frontend guides are created by Storybook maintainers and peer-reviewed by the open source community.

storybook.js.org

https://nextjs.org/

 

Next.js by Vercel - The React Framework

Next.js by Vercel is the full-stack React framework for the web.

nextjs.org

 

반응형