반응형
반응형

커스텀훅은 개발자가 직접 정의한 재사용 가능한 함수입니다.

 

useOnlineStatus.js

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

 

App.js

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ 온라인' : '❌ 연결 안 됨'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ 진행사항 저장됨');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? '진행사항 저장' : '재연결 중...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

위 예제를 보면 연결이 됐냐 안 됐냐에 대한 상태관리를 하는데 이걸 이부분만 아니라 다른 곳에서도 사용한 경우 훅으로 만들어서 연결 됐는지 안 됐는지에 대한 처리를 간결하게 처리할 수 있고 유지보수가 용이해집니다.

 

커스텀훅의 경우 use를 prefix로 붙여야 동작합니다.


 

App.js (또다른 예제)

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}

Form을 이용한 또 다른 예제를 살펴보겠습니다. 여기에선 기존에 e.target.value를 이용해 상태값에 넣는 동일 역할을 하는 함수가 있습니다. 이걸 커스텀훅을 이용해 줄여보도록 하겠습니다.

 

 

useFormInput.js (또다른 예제)

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

 

 

App.js (또다른 예제)

import { useFormInput } from './useFormInput.js';

export default function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');

  return (
    <>
      <label>
        First name:
        <input {...firstNameProps} />
      </label>
      <label>
        Last name:
        <input {...lastNameProps} />
      </label>
      <p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>
    </>
  );
}

useFormInput을 두개를 호출했는데 이럴 경우 state가 1개로 공유하는 게 아니라 각각 들어가게 됩니다. state를 공유하는 게 아닌 state 저장 로직을 공유함으로 독립적이다.

 

커스텀 Hook 안의 코드는 컴포넌트가 재렌더링될 때마다 다시 돌아갈 겁니다. 이게 바로 커스컴 Hook이 (컴포넌트처럼) 순수해야하는 이유 입니다.

 

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Show
        </button>
      )}
    </section>
  );
}

만약 state를 공유해야하면 위 예제 처럼 부모 state를 정의하고 그 state를 props로 전달하면 된다.

 

또다른 예제

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  // 이 Effect는 나라별 도시를 불러옵니다.
  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]);

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  // 이 Effect 선택된 도시의 구역을 불러옵니다.
  useEffect(() => {
    if (city) {
      let ignore = false;
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [city]);

  // ...
  
  /** ──── custom hook으로 변환 ────**/
  function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    if (url) {
      let ignore = false;
      fetch(url)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setData(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [url]);
  return data;
}

 

커스텀 훅이라는 건 간단하게 설명하면 Hook이 들어간 함수라고 생각하면 된다.

 

다양한 상황이 존재한다.

  1. 어떤 상태값(state, ref)을 전달했을 때 변환시킨다.
  2. 어떤 상태값이나 값(객체나 함수 다 포함)을 리턴 받는다.
반응형
반응형

📝Reducer

컴포넌트가 복잡해지면 컴포넌트의 state가 업데이트되는 다양한 경우를 한눈에 파악하기 어려워질 수 있습니다

컴포넌트 내부에 있는 state 로직을 컴포넌트 외부의 “reducer”라고 하는 단일 함수로 옮길 수 있습니다

reducer는 state를 다루는 다른 방법입니다

 

useState 사용한 예제

import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([...tasks, {
      id: nextId++,
      text: text,
      done: false
    }]);
  }

  function handleChangeTask(task) {
    setTasks(tasks.map(t => {
      if (t.id === task.id) {
        return task;
      } else {
        return t;
      }
    }));
  }

  function handleDeleteTask(taskId) {
    setTasks(
      tasks.filter(t => t.id !== taskId)
    );
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false },
];

위 예제를 reducer를 이용해 바꿔보겠습니다

 

state에서 보낼 데이터 정보 추출

function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId
  });
}

첫번째 이벤트 리스너에 있는 로직에 필요한 데이터를 선별한 후 dispatch라는 함수를 통해 보내주는 역할만 합니다

여기에서 type으로는 어떤 기능을 할 지 그리고 나머지에는 보내고 싶은 형태로 데이터를 보냅니다

 

 

reducer에 state에서 사용했던 로직 옮기기

function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [...tasks, {
      id: action.id,
      text: action.text,
      done: false
    }];
  } else if (action.type === 'changed') {
    return tasks.map(t => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === 'deleted') {
    return tasks.filter(t => t.id !== action.id);
  } else {
    throw Error('Unknown action: ' + action.type);
  }
}

type을 보내준 이유는 여기에서 사용하려고 함입니다 여기 예제에서는 if-else문으로 되어있지만 switch-case로 대부분 사용합니다 tasks기존 데이터(initialTasks)가 들어가고 action에서는 dispatch로 보낸 데이터가 들어가게 됩니다

 

useReducer를 이용한 예제로 변경한 코드

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

이걸 하나의 코드로 표현하면 위와 같습니다 

 

  • useReducer로 내가 만든 reducer(로직처리 부분)과 초기값을 연결시킵니다
  • dispatch를 통해 해당 reducer로 사용할 값을 보냅니다

 

📝useRef

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

ref는 참조에 많이 쓰이는 값이며 특징은 아래와 같습니다

  • ref.current을 이용해 해당 ref의 current 값에 접근할 수 있습니다 이 값은 의도적으로 변경할 수 있으며 읽고 쓸 수 있습니다
  • state와 달리 ref를 변경하면 다시 렌더링 되지 않습니다
  • Event Handler에게만 필요한 정보이고 변경이 일어날 때 리렌더링이 필요하지 않다면, ref를 사용하는 것이 더 효율적입니다 (대부분 ref 접근은 이벤트 핸들러 안에서 일어납니다)
  • React가 관리하는 DOM 요소에 접근해야 할 때 사용됩니다

 

import React, { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  // 렌더링 중에 ref.current를 읽는 경우
  if (inputRef.current) {
    console.log('렌더링 중에 ref.current를 읽음:', inputRef.current);
  }

  return <input ref={inputRef} />;
}

export default MyComponent;

useRef는 렌더링중에 읽거나 쓰기를 하지 않아야합니다

 

 

 

DOM 여러개 ref로 관리하기

import { useRef, useState } from "react";

export default function CatFriends() {
  const itemsRef = useRef(null);
  const [catList, setCatList] = useState(setupCatList);

  function scrollToCat(cat) {
    const map = getMap();
    const node = map.get(cat);
    node.scrollIntoView({
      behavior: "smooth",
      block: "nearest",
      inline: "center",
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // 처음 사용하는 경우, Map을 초기화합니다.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToCat(catList[0])}>Tom</button>
        <button onClick={() => scrollToCat(catList[5])}>Maru</button>
        <button onClick={() => scrollToCat(catList[9])}>Jellylorum</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat, node);
                } else {
                  map.delete(cat);
                }
              }}
            >
              <img src={cat} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

function setupCatList() {
  const catList = [];
  for (let i = 0; i < 10; i++) {
    catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
  }

  return catList;
}

배열값이 있어서 순회하면서 화면에 출력하는데 해당 출력된 값에 ref로 접근해야 할 때 ref안에 useRef를 생성할 수 없기 때문에 useRef에 Map을 넣고  ref에서 node라고 ref정보를 담은 걸 key,value로 담아서 useRef를 관리한다

 

 

 

다른 컴포넌트에서 ref 참조하기

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

일반적으로 ref만 사용하면 다른 컴포넌트 ref에 접근할 수 없는데 forwordRef를 이용해 다른 컴포넌트의 ref값을 참조할 수 있습니다

 

 

📝useEffect

작동할 내용과 어떤 값을 감시할 지 정합니다 그 후 감시할 값이 변할 경우 useEffect에 작성한 내용이 작동하게 됩니다

변화되었는지에 대해서는 Object.is라는 것을 통해 주소값이 변동되었는지로 감시합니다 그렇기 때문에 감시할 값이 Object인 경우 리렌더링할 때 Object 내용은 같아도 주소값이 계속 변동되기 때문에 바뀌었다고 판단해서 계속적인 리렌더링이 일어날 수 있습니다

 

  1. 기본적으로 Effect는 모든 commit 이후에 실행됩니다
    • 컴포넌트가 렌더링 될 때마다 React는 화면을 업데이트한 다음 useEffect 내부의 코드를 실행합니다 다시 말해, useEffect는 화면에 렌더링이 반영될 때까지 코드 실행을 “지연”시킵니다
  2. 필요한 경우 클린업 함수 추가해서 해제해 줘야합니다

 

useEffect 예제

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  // isPlaying 값이 변경되었다는 걸 감지했을 때 동작
  useEffect(() => {
    if (isPlaying) {
      console.log('video.play() 호출');
      ref.current.play();
    } else {
      console.log('video.pause() 호출');
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? '일시 정지' : '재생'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

useEffect은 어떤 값이 변화되었다는 걸 감지해서 감지했을 때 안에 있는 익명함수를 실행시킵니다

 

useEffect 사용 방법

useEffect(() => {
  // 모든 렌더링 후에 실행됩니다
});

useEffect(() => {
  // 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);

useEffect(() => {
 // 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);

 

useEffect 클린업

/** 커넥션 해제 **/
useEffect(() => {
  const connection = createConnection();
  connection.connect();
  return () => {
    connection.disconnect();
  };
}, []);

/** 애니메이션 해제 **/
useEffect(() => {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

useEffect가 어떤 것을 구독한다면, 클린업 함수에서 구독을 해지해야 합니다

예를 들면 useEffect가 어떤 요소를 애니메이션으로 표시하는 경우, 클린업 함수에서 애니메이션을 초기 값으로 재설정해야 합니다

 

 

클린업 예제

function App() {

    const [data,setData]= useState();
    const [input, setInput] = useState('')


    /** clean-up 미사용 **/
    // useEffect(()=> {
    //
    //     const getData = async () => {
    //         const resp = await fetch('https://api.sampleapis.com/coffee/hot');
    //         const json = await resp.json();
    //         setData(json[0][input]);
    //     }
    //     getData();
    //
    // },[input])

    useEffect(()=> {
        let ignore = false;

        const getData = async () => {
            const resp = await fetch('https://api.sampleapis.com/coffee/hot');
            const json = await resp.json();
            if (!ignore) {
                setData(json[0][input]);
            }

        }

        getData();

        return () => {
            ignore = true;
        };

    },[input])

    return (
        <div>
            {data}
            <br/>
            <select onChange={(e)=>{setInput(e.target.value)}}>
                <option value="title">title</option>
                <option value="description">description</option>
            </select>
        </div>

    );

}

 

DataFetch가 있고 그 이후 그 데이터를 JSX로 보여주는데 동작과정은 아래와 같다

  • useId 변경 → 언마운트로 ignore true 동작 → DataFetch → setData로 리렌더링

 

fetch에서 clean-up 작업을 한 이유는 예를 들면 네트워크가 안 좋은데 select box를 엄청나게 바꾸면 네트워크 요청은 여러개가 나가고 리렌더링작업은 큐처럼 쌓이게 되어 뒤늦게 {data}가 큐에 쌓인대로 API요청한 회수만큼 와따가따하게 된다 clean-up 작업을 한 경우는 언마운트시 작동함으로 전 네트워크 요청에 대해서 ignore true값을 줘서 리렌더링 안 하게 한다

 

동작 과정은 아래와 같다

  • Select box 변경 → 1번 네트워크 요청 → 1번 네트워크 요청중에 Select box 또 변경 → 1번 네트워크 요청 결과를 await중에 clean-up 내용 작동으로 ignore가 true로 변경되어서 데이터 받아와도 setData 작동 안 해서 변경 없음 → 2번 네트워크 요청 await후 input의 변경이 없기 때문에 ignore은 false상태이기 때문에 리렌더링이 된다

 

useEffect 사용할 때 주의할 점이 있다

  • useEffect의 경우 렌더링 된 이후에 작동하기 때문에 렌더링 전에 작동시켜야하면 서버사이드에서 해결해줘야한다
  • useEffect만 사용해서 데이터를 fetch하는 경우 미리 데이터를 로드하거나 캐시하지 않기 때문에 느릴 수도 있다

위와 같은 문제점을 해결하기 위해서는 일반적으로 프레임워크에 있는 내장 데이터 패칭기능이나 오픈 소스 솔루션인 React Query, useSWR를 사용하면 네트워크 응답을 캐싱하며 네트워크를 기본적으로 최적화하니 사용하는게 좋다

 

 

useEffect 잘 사용하기

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // 🔴 피하세요: 중복된 state 및 불필요한 효과
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ getFilteredTodos()가 느리지 않다면 괜찮습니다.
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ✅ todos 또는 filter가 변경되지 않는 한 다시 실행되지 않습니다.
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

props의 todos랑 filter랑 변경되면서 다시 해당 함수인 TodoList를 실행하기 때문에 굳이 state로 관리할필요 없고 필요사항에만 메모이제이션해서 계산하게 하면 더욱 좋다

 

  • 어떤 경우 메모이제이션을 해야할까?
    • 전체적으로 기록된 시간이 상당한 양(예: 1ms 이상)으로 합산되면 해당 계산을 메모이제이션하는 것이 좋습니다가장 정확한 시간을 얻으려면 프로덕션용 앱을 빌드하고 사용자가 사용하는 것과 같은 기기에서 테스트해야합니다

 

function App() {
    const [firstName, setFirstName] = useState('Taylor');
    const [lastName, setLastName] = useState('Swift');
    // 위 useState 값이 변경되면서 전체를 실행하기 때문에 state로 상태관리를 할 필요도 없다
    const fullName = firstName + ' ' + lastName; 
    
    // 🔴 피하세요: 중복된 state 및 불필요한 Effect
    //const [fullName, setFullName] = useState('');
    //useEffect(() => {
    //    setFullName(firstName + ' ' + lastName);
    //}, [firstName, lastName]);

    return (
        <div>
            <input type="text" onChange={(e)=>setFirstName(e.target.value)} />
            <input type="text" onChange={(e)=>setLastName(e.target.value)} />
            {fullName}
        </div>
    );
}

입력따위로 set하는 경우가 따로 있지 않는 경우는 state로 만들 필요가 없다

 

// ProfilePage 컴포넌트: userId를 prop으로 받아서 Profile 컴포넌트에 전달
function ProfilePage({ userId }: {userId:string}) {
    return (
        <Profile
            userId={userId}
            key={userId} // userId가 변경될 때마다 새로운 Profile 컴포넌트가 마운트됨
        />
    );
}

// Profile 컴포넌트: userId를 prop으로 받아서 사용
function Profile({ userId }:{userId:string}) {
    const [comment, setComment] = useState('');

    return (
        <div>
            <h1>Profile for user: {userId}</h1>
            <textarea
                value={comment}
                onChange={(e) => setComment(e.target.value)}
                placeholder="Leave a comment"
            />
        </div>
    );
}



function App() {
    const [userId, setUserId] = useState('user1');

    return (
        <div>
            <button onClick={() => setUserId('user1')}>User 1</button>
            <button onClick={() => setUserId('user2')}>User 2</button>
            <ProfilePage userId={userId}  />
        </div>
    );
}

userId값이 바뀌어서 props값이 달라지면서 리렌더링이 되는데 일반적으로 React는 동일한 컴포넌트가 같은 위치에 렌더링 될 때 state를 보존하기 때문에 comment값은 초기화가 안 됩니다 그렇기 때문에 key를 이용해 서로 다른 컴포넌트라는 걸 인식시켜줘야합니다

 

일반적으로 props가 계속 바뀌면 key 설정을 해주는게 맞습니다

 

function List({ items }: {items: Array<any>}) {
    // const [selection, setSelection] = useState<any>(null);

    // 🔴 피하세요: Effect에서 prop 변경 시 state 조정하기 (방법1)
    // useEffect(() => {
    //     setSelection(null);
    // }, [items]);

    // 더 좋습니다: 렌더링 중 state 조정 (방법2)
    // const [prevItems, setPrevItems] = useState(items);
    // if (items !== prevItems) {
    //     setPrevItems(items);
    //     setSelection(null);
    // }

    // return (
    //     <div>
    //         <ul>
    //             {items.map(item => (
    //                 <li key={item.id} onClick={() => setSelection(item)}>
    //                     {item.name}
    //                 </li>
    //             ))}
    //         </ul>
    //         <div>
    //             Selected: {selection ? selection.name : 'None'}
    //         </div>
    //     </div>
    // );

    const [selectedId, setSelectedId] = useState(null);

    // ✅ 최고예요: 렌더링 중에 모든 것을 계산 (방법3)
    const selection = items.find(item => item.id === selectedId) ?? null;


    return (
        <div>
            <ul>
                {items.map(item => (
                    <li key={item.id} onClick={() => setSelectedId(item.id)}>
                        {item.name}
                    </li>
                ))}
            </ul>
            <div>
                Selected: {selection ? selection.name : 'None'}
            </div>
        </div>
    );
}

function App() {
    const [items, setItems] = useState(items1);

    return (
        <div>
            <button onClick={() => setItems(items1)}>Set Items 1</button>
            <button onClick={() => setItems(items2)}>Set Items 2</button>
            <List items={items}/>
        </div>
    );

}

key 설정을 따로 못하는 경우 같은 위치 컴포넌트의 state 초기화 시켜야할 때는 위와 같은 방법을 이용할 수 있습니다

 

/** bad code **/
function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 피하세요: 서로를 트리거하기 위해서만 state를 조정하는 Effect 체인
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    } else {
      setCard(nextCard);
    }
  }

  // ...
  
  
  /** right code **/
  function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ 렌더링 중에 가능한 것을 계산합니다.
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // ✅ 이벤트 핸들러에서 다음 state를 모두 계산합니다.
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }

  // ...

웬만해서 Effect를 남발하지말고 Effect 없이 해결할 수 있는지 파악한다 그리고 같은 맥락의 있는 경우 Effect를 하나로 묶어주는게 더 좋다 위 예시는 여러 개의 useEffect 훅을 사용하여 상태 업데이트를 체이닝하면 비효율적이고 유지보수가 어려운 코드를 만들 수 있다는 것을 보여줍니다

 

import React from 'react';

function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      onChange(true);
    } else {
      onChange(false);
    }
  }

  return (
    <div>
      <button onClick={handleClick}>{isOn ? 'ON' : 'OFF'}</button>
      <div
        onDragEnd={handleDragEnd}
        style={{ width: '100px', height: '100px', backgroundColor: 'lightgray' }}
      />
    </div>
  );
}

export default Toggle;


import React, { useState } from 'react';
import Toggle from './Toggle';

function App() {
  const [isOn, setIsOn] = useState(false);

  function handleToggleChange(nextIsOn) {
    setIsOn(nextIsOn);
  }

  return (
    <div>
      <Toggle isOn={isOn} onChange={handleToggleChange} />
      <div>Toggle is {isOn ? 'ON' : 'OFF'}</div>
    </div>
  );
}

export default App;

자식이 부모에게 명령을 내리거나 데이터를 전달하려면 부모로부터 제어권(props)들을 넘겨받아야한다

 

 

반응형
반응형

📝React 이벤트 버블링

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

부여된 JSX 태그 내에서만 실행되는 onScroll을 제외한 React 내의 모든 이벤트는 전파됩니다

이걸 막기 위해서는 e.stopPropagation()으로 버블링을 안 시킬 수 있습니다

 

애초에 이렇게 만드는게 이상한 듯 싶지만 Button 외를 클릭시에 어떤 동작을 하게 할 때 필요합니다

 

예제 코드

https://codesandbox.io/p/sandbox/react-dev-qqwck7?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

https://codesandbox.io/p/sandbox/react-dev-qqwck7?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

codesandbox.io

 

 

이벤트 버블링 참고내용

https://mondaymonday2.tistory.com/765

 

[JavaScript] 자바스크립트 이벤트 동작 과정 (이벤트 버블링, 이벤트 캡처, 이벤트 위임)

자바스크립트에서 이벤트가 동작되는 과정에는 두가지 방식이 있습니다. 기본적으로 이벤트 버블링으로 동작합니다. 📝이벤트 버블링 이벤트 버블링이란 하위 태그에서 이벤트가 발생할시 위

mondaymonday2.tistory.com

 

 

📝Props Drilling

보통의 경우 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달하는데 중간에 많은 컴포넌트를 거쳐야하거나 하는경우 A의 값을 하위 B로 전달 B의 값을 하위 C로 전달... 계속적인 컴포넌트에 Props전달하는 경우를 의미합니다

 

 

📝Context를 이용해 Props Drilling 해결하기

 

 

Context를 사용하면 props를 전달하지 않아도 부모 컴포넌트에서 자식 컴포넌트에 한번에 보낼 수 있게 됩니다 간단하게 이야기하면 공통영역이 있어서 거기서 가져온다고 생각하면 됩니다

 

아래에서는 context API를 이용해 theme 즉 다크모드, 라이트모드 변경하는 예제하는 걸 보여드리겠습니다

 

다크모드인지 라이트모드인지에 대해서는 공통으로 최상단에서 관리해야하고 위에서 아래로 모든 컴포넌트에 계속 전달하기 어렵기 때문에 context API를 이용하는게 좋습니다

 

ThemeContext.js

import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light'); // 초기 테마는 'light'

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

createContext로 context를 사용하기 위해 만듭니다

 

ThemeToggleButton

import React from 'react';
import { ThemeProvider, useTheme } from './ThemeContext';

const ThemeToggleButton = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);

  const themeStyles = {
    backgroundColor: theme === 'light' ? '#FFF' : '#333',
    color: theme === 'light' ? '#333' : '#FFF',
  };

  return (
    <button style={themeStyles} onClick={toggleTheme}>
      Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
};

useContext로 Context에 선언된 state와 정보들을 가져오고 Toggle 버튼으로 theme값을 변경시키게 수정합니다

 

App.js

const App = () => {
  return (
    <ThemeProvider>
      <div>
        <h1>Hello, React Context API!</h1>
        <ThemeToggleButton />
      </div>
    </ThemeProvider>
  );
};

export default App;

전체 앱에 <ThemeProvider>로 감싸서 전체가 인식할 수 있게 합니다

 

 

context 사용 예시

  • context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있습니다.
  • 로그인한 사용자를 알아야 하는 컴포넌트가 많을 수 있습니다. Context에 놓으면 트리 어디에서나 편하게 알아낼 수 있습니다.
  • 애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 됩니다. 아래 멀리 떨어진 많은 컴포넌트가 그 값을 변경하고싶어할 수 있습니다.

 

context 문제점

  • 성능 이슈
    • Context API는 컴포넌트 트리의 모든 수준에서 데이터를 전달할 수 있지만, 종종 성능 문제를 일으킬 수 있습니다. Context를 사용하면 Context의 데이터가 변경될 때마다 해당 Context를 사용하는 모든 컴포넌트가 리렌더링됩니다. 이는 특히 대규모 애플리케이션에서 성능 저하를 유발할 수 있습니다.
  • 스케일링 문제
    • 작은 규모의 프로젝트에서는 효과적이지만, 애플리케이션이 커지면서 상태 관리가 복잡해질 때 Context API만으로는 관리가 어려워질 수 있습니다. 복잡한 상태 로직을 관리하고, 다양한 상태 변화를 효율적으로 처리하기 어려울 수 있습니다.
  • 구조적 한계
    • Context API는 전역 상태 관리보다는 주로 테마, 사용자 설정과 같이 고정적이고 간단한 데이터를 공유하는 데에 적합합니다. 복잡한 상태 변화와 비즈니스 로직을 포함하는 더 동적인 상태 관리에는 한계가 있습니다.

 

 

 

이러한 문제점을 해결하기 위해 다른 전역상태를 관리하는 툴을 많이 쓰는데 recoil,redux toolkit 등... 개인적으로 가장 많이 사용하는 redux toolkit을 사용하는게 좋습니다

 

반응형
반응형

📝useState (상태 관리)

function MyButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}

리액트는 필요한 부분만 렌더링하기 위한 리액트만의 방식이 있습니다 useState라는 걸 이용해 값을 저장하고 그 값이 Setter로 변화시켰을 때 감지해 그 부분만 렌더링시키게 합니다


가장 많이 사용하는 것이라 필수적으로 익혀둬야합니다

 

  • 조건문, 반복문 또는 기타 중첩 함수 내부에서는 훅을 호출할 수 없습니다
  • 명명 규칙으로는 명사를 따르며 set + State변수명으로 setter 함수를 만드시면 됩니다

 

예제 코드

https://codesandbox.io/p/sandbox/react-dev-ryf69v?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

https://codesandbox.io/p/sandbox/react-dev-ryf69v?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

codesandbox.io

 

state가 서로 독립적이기 때문에 개별적으로 돌아갑니다

 

예제 코드

https://codesandbox.io/p/sandbox/react-dev-5rx42k?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

https://codesandbox.io/p/sandbox/react-dev-5rx42k?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

codesandbox.io

 

📝useState (객체, 배열) 

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

state에 저장한 자바스크립트 객체와 배열은 어떤 것이라도 읽기 전용인 것처럼 다루어야 합니다

 

렌더링시 상태 값에 변화를 주려면 setState에 새로운 객체 및 배열을 할당해줘야하고 ...로 기존 데이터를 유지시키며 다른 데이터를 덮어씌워서 변화를 줄 수 있습니다

 

  비선호 (배열 변경) 선호 (새 배열 반환)
추가 push, unshift concat, [...arr] 전개 연산자
제거 pop, shift, splice filter, slice
교체 splice, arr[i] = 값 할당 map
정렬 reverse, sort 배열 복사한 이후에 처리

React에서 선호하는 방식으로 배열 함수를 사용해서 새로운 배열을 만들어서 setState에 할당해주면 됩니다

 

📝useState 동작과정

state 변화가 일어날 때 스냅샷을 만든 다음 state 변화에 대한 내용을 작성합니다 그 이후 현재 화면과 스냅샷 화면을 비교해서 해당 부분만 업데이트합니다

 

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

/** 첫번째 클릭 **/
<button onClick={() => {
  setNumber(0 + 1);
  setNumber(0 + 1);
  setNumber(0 + 1);
}}>+3</button>

/** 두번째 클릭 **/
<button onClick={() => {
  setNumber(1 + 1);
  setNumber(1 + 1);
  setNumber(1 + 1);
}}>+3</button>

위코드를 실행시킬 경우 +3이 아니라 가장 마지막에 있는 변화만 적용시킵니다 그래서 1이 보이게 됩니다

 

예제 코드

https://codesandbox.io/s/ffhp6h?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>
    </>
  )

시간의 경과가 있어도 동일하게 작동합니다

 

요약하자면 처음 클릭할 때 state는 유지되고 그 context가 있기 때문에  3초가 지나도 alert에도 0이 찍히게 되고 렌더링 한 이후에는 state가 바뀌게 되고 서로 다른 context가 존재하기 때문에 서로 간섭 안 해서 사이드이펙트가 일어나지 않습니다

 

예제 코드

https://codesandbox.io/s/sz392q?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

 

📝같은 자리의 컴포넌트 state 보존 (state 특징)

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

여기서 삼항연산자로 true일경우 <Counter isFancy={true}/> false일 경우 <Counter isFancy={false}/>을 보여주는데 체크박스로 isFancy값을 변경시켜도 Counter에 있는 count값은 변화하지 않는데 같은 자리의 같은 컴포넌트는 state를 보존합니다

 

 

 

예제 코드

https://codesandbox.io/s/6f2zp8?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

📝같은 자리의 컴포넌트 state 보존 안 시키기 (state 특징)

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

이건 위에 코드랑 비슷해보이지만 삼항연산자로 되어있는게 아니라서 위치가 바뀌기 때문에 state가 유지가 안 됩니다

 

 

예제 코드

https://codesandbox.io/s/g9f9r8?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

key를 주게 되면 위치로 판단하는게 아니라 key로 찾아가기 때문에 삼항연산자를 써도 state가 초기화됩니다

 

예제 코드

https://codesandbox.io/p/sandbox/react-dev-wvssxq?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

https://codesandbox.io/p/sandbox/react-dev-wvssxq?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

codesandbox.io

 

 

 

 

📝렌더링 전에 동일 state 변수 여러번 업데이트

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 15);
        setNumber(n => n + 10);
      }}>+N</button>
    </>
  )
}

흔한 사례는 아니지만 만약 동일한 Context내에서 state 변수를 여러번 업데이트 하려면 위와 같이 사용하면 됩니다

여기서 n은 변화된 값을 가지고 있는 변수라고 생각하시면 됩니다 그렇기 때문에 원래라면 10이 보여야하지만 1+15+10을 다 더한 26이 보이게 됩니다

 

📝연관된 state 그룹화 (React State 팁)

/** 변경전 **/
const [x, setX] = useState(0);
const [y, setY] = useState(0);

/** 변경후 **/
const [position, setPosition] = useState({ x: 0, y: 0 });

두 개의 state 변수가 항상 함께 변경된다면, 단일 state 변수로 통합하는 것이 좋습니다

 

📝state 중복 피하기 (React State 팁)

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

현재는 선택된 항목을 selectedItem state 변수에 객체로 저장합니다. 그러나 이는 좋지 않습니다. selectedItem의 내용이 items 목록 내의 항목 중 하나와 동일한 객체입니다. 이는 항목 자체에 대한 정보가 두 곳에서 중복되는 것입니다

 

“Choose”를 클릭한  이를 편집할 경우, 입력이 업데이트되지만, 하단의 라벨에는 편집 내용이 반영되지 않습니다 

 

예제 코드

https://codesandbox.io/s/qwm2gy?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

 

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

selectedItem을 selectedItemId로 변경하고 id값으로 item을 찾으면 item이 변경되어도 똑같이 변경되게 되는 것이죠

 

 

예제 코드 (개선)

https://codesandbox.io/s/8mhwpl?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

📝깊게 중첩된 state 피하기 (React State 팁)

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

만일 state가 쉽게 업데이트하기에 너무 중첩되어 있다면, “평탄”하게 만드는 것을 고려하세요

 

예제 코드

https://codesandbox.io/s/vmm64f?file=%2Fsrc%2Fplaces.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

 

개선된 코드

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Moon',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

 

예제 코드

https://codesandbox.io/p/sandbox/react-dev-q63dwl?file=%2Fsrc%2Fplaces.js&utm_medium=sandpack

 

https://codesandbox.io/p/sandbox/react-dev-q63dwl?file=%2Fsrc%2Fplaces.js&utm_medium=sandpack

 

codesandbox.io

 

📝Props (데이터 전달)

export default function MyApp() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <h1>Counters that update together</h1>
      <MyButton count={count} onClick={()=>handleClick()} />
      <MyButton count={count} onClick={handleClick} />
      {/* 잘못된 전달 <MyButton count={count} onClick={handleClick()} /> */} 
    </div>
  );
}

function MyButton({ count, onClick }) {
  return (
    <button onClick={onClick}>
      Clicked {count} times
    </button>
  );
}

부모에서 자식 컴포넌트에 값을 전달하기위해 파라미터처럼 값을 보내줘야하는데 이러한 인자값 정의를 Props라고합니

다 (Property를 보낸다라는 느낌)

 

JSX에서 작성된 Props는 JavaScript 객체의 키가 됩니다 그래서 변수명에 대시를 포함하거나 class처럼 예약어를 사용할 수 없습니다 그래서 대부분 camelCase를 사용합니다

 

React는 {} 중괄호에 있는 건 다 실행시킵니다 그렇기 때문에 함수() 이런식으로 넣는 경우 바로 동작하게 됩니다 

Props처럼 컴포넌트에 매개변수를 넣어줄 수 있는데 함수를 넘길 경우 ()라는 실행한 값이 아닌 함수의 참조값이나 익명함수를 전달해줘야합니다

 

예제 코드
https://codesandbox.io/p/sandbox/react-dev-jmpn67?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

https://codesandbox.io/p/sandbox/react-dev-jmpn67?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

codesandbox.io

 

📝Props 기본값 설정

function Avatar({ person, size = 100 }) {
  // ...
}

위 코드처럼 기본값 설정할 수 있습니다

 

📝Props 팁

function Profile({ person, size, isSepia, thickBorder }) {
  return (
    <div className="card">
      <Avatar
        person={person}
        size={size}
        isSepia={isSepia}
        thickBorder={thickBorder}
      />
    </div>
  );
}


function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}

위 코드와 같이 props의 키와 값을 동일하게 설정하면 코드를 간결화할 수 있습니다

... spread 문법은 많이 사용하기 때문에 익숙해지길 바랍니다

 

export default function StoryTray({ stories }) {
  stories.push({
    id: 'create',
    label: 'Create Story'
  });

  return (
    <ul>
      {stories.map(story => (
        <li key={story.id}>
          {story.label}
        </li>
      ))}
    </ul>
  );
}

props로 받는 데이터는 그 자체를 변형시켜서는 안 됩니다 

 

📝자식을 JSX로 전달 (children)

/** HTML 방식 **/
<div>
  <img />
</div>

/** React 방식 **/
<Card>
  <Avatar />
</Card>

컴포넌트를 중첩해서 쓰고 싶을 때가 있습니다 children을 사용하면 이를 쉽게 사용할 수있습니다

 

import Avatar from './Avatar.js';

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}

Card 컴포넌트 안에 들어가는 하위 것들을 children으로 퉁쳐서 들어갈 수 있습니다

반응형
반응형

📝컴포넌트, JSX, TSX

/** 예제 코드 **/
export default function AboutPage() {
  return (
    <>
      <h1>About</h1>
      <p>Hello there.<br />How do you do?</p>
    </>
  );
}

function Main () {
  return(
    <AboutPage/>
  );
}


/** 외부 컴포넌트 가져오기 **/
import Gallery from './Gallery.js';

export default function App() {
  return (
    <Gallery />
  );
}

React에서는 HTML을 여러 조각으로 나눠서 재활용 및 유지보수할 수 있게 만들 수 있습니다 이걸 컴포넌트라고 합니다

React에서 HTML을 리턴해주는 파일의 역할 및 확장자JSX, TSX(타입스크립트 적용시)라고 합니다

  • JSX는 HTML보다 엄격해서 같은 태그로 닫아야합니다 → 하나의 부모 태그
    • 보통 <div> 태그로 닫지만 <div> 태그로 안 닫는 경우는 <> </> 이러한 태그로라도 닫아야합니다 
  • 대문자로 시작해야합니다
  • 다른 파일에서 사용하기 위해서는 export default로 해당 함수를 내보내야합니다

 

예제 코드

https://codesandbox.io/p/sandbox/react-dev-nh2zj3?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

https://codesandbox.io/p/sandbox/react-dev-nh2zj3?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

codesandbox.io

 

📝변수 화면 노출

/** 예제 코드 **/
return (
  <h1>
    {user.name}
  </h1>
);

export default function TodoList() {
  return (
    <h1>To Do List for {formatDate(today)}</h1>
  );
}

JSX에서 HTML 코드 안에 자바스크립트 코드를 안에 넣을 수 있는데 {}안에 넣으면 됩니다

변수를 넣었으니 변수값이 출력되는 거고 데이터 format이라든가 로직이 들어갈수도 있습니다

 

📝조건부 렌더링

/** ?을 이용한 삼항연산자 조건부 렌더링 **/
<div>
  {isLoggedIn ? (
    <AdminPanel />
  ) : (
    <LoginForm />
  )}
</div>

/** null & undefined가 아닌 값이 들어있는 경우 조건부 렌더링 **/
<div>
  {isLoggedIn && <AdminPanel />}
</div>

조건부 렌더링도 가능합니다 위에서 설명한 것처럼 {} 안에 사용하면 됩니다

조건부 렌더링의 경우 더 짧게 사용하기 위해 Javascript 문법을 사용해 표현하는 경우가 많습니다

 

&&와 그 외에 논리연산자에 더 자세히 보려면 제가 쓴 글 참고 바랍니다

https://mondaymonday2.tistory.com/848

 

[JavaScript] 자바스크립트 falsy 값, 논리연산자 축약, Null 및 undefined 변수 처리 ( ||=, &&=, ?? )

📝falsy 값falsy값이란 false, null, undefined, 0, NaN, 빈 문자열('') 등의 값을 의미한다  📝||= , &&= /** || **/console.log( "" || undefined || null || "익명"); // 익명console.log( "" || "Hello" || null || "익명"); // Hellolet nu

mondaymonday2.tistory.com

 

예제 코드

https://codesandbox.io/s/zjtyvn?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

📝배열 렌더링

const listItems = products.map(product =>
  <li key={product.id}>
    {product.title}
  </li>
);

return (
  <ul>{listItems}</ul>
);

배열로 데이터가 내려오면서 배열안에 데이터를 같은 형태의 HTML로 노출할 때 많이 사용합니다

map함수를 이용해 배열값을 렌더링 시킬 수 있습니다

 

const listItems = chemists.map(person =>
  <li>...</li> // 암시적 반환!
);

const listItems = chemists.map(person => { // 중괄호
  return <li>...</li>;
});

화살표 함수는 => 바로 뒤에 식을 반환하기 때문에 return이 필요하지 않지만 => 뒤에 {} 중괄호가 오는 경우는 return을 명시해야합니다

 

예제코드

https://codesandbox.io/s/2dmxxm?file=%2Fsrc%2FApp.js&utm_medium=sandpack

 

react.dev - CodeSandbox

react.dev using react, react-dom, react-scripts

codesandbox.io

 

📝배열 렌더링 Key 사용 이유

import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const listItems = people.map(person =>
    <li key={person.id}>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return (
    <article>
      <h1>Scientists</h1>
      <ul>{listItems}</ul>
    </article>
  );
}

Key는 각 컴포넌트가 어떤 배열 항목에 해당하는지 React에 알려주어 나중에 일치시킬 수 있도록 합니다

이는 배열 항목이 정렬 등으로 인해 이동하거나 삽입되거나 삭제될 수 있는 경우 중요해집니다

key를 잘 선택하면 React가 정확히 무슨 일이 일어났는지 추론하고 DOM 트리에 올바르게 업데이트 하는데 도움이 됩니다

 

데스크탑의 파일에 이름이 없다고 상상해 보세요. 대신 첫 번째 파일, 두 번째 파일 등 순서대로 파일을 참조할 것입니다. 익숙해질 수도 있지만, 파일을 삭제한다면 혼란스러워질 수도 있습니다. 두 번째 파일이 첫 번째 파일이 되고 세 번째 파일이 두 번째 파일이 되는 식으로 말이죠.

폴더의 파일 이름과 배열의 JSX key는 비슷한 용도로 사용됩니다.

이를 통해 형제 항목 간에 항목을 고유하게 식별할 수 있습니다.

잘 선택된 key는 배열 내 위치보다 더 많은 정보를 제공합니다. 재정렬로 인해 위치가 변경되더라도 key는 React가 생명주기 내내 해당 항목을 식별할 수 있게 해줍니다

 

 

Key 특징

  • key는 형제 간에 고유해야 합니다 하지만 같은 key를 다른 배열의 JSX 노드에 동일한 key를 사용해도 괜찮습니다.
  • key는 변경되어서는 안 되며 그렇게 되면 key는 목적에 어긋납니다! 렌더링 중에는 key를 생성하지 마세요.

 

Key 주의점

  • 인덱스를 key로 사용하면 종종 미묘하고 혼란스러운 버그가 발생하니 사용하지마세요
  • 마찬가지로 key={Math.random()}처럼 즉석에서 key를 생성하지 마세요. 이렇게 하면 렌더링 간에 key가 일치하지 않아 모든 컴포넌트와 DOM이 매번 다시 생성될 수 있습니다. 속도가 느려질 뿐만 아니라 리스트 항목 내부의 모든 사용자의 입력도 손실됩니다. 대신 데이터 기반의 안정적인 ID를 사용하세요

 

📝이벤트 핸들링

function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }

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

React의 경우 함수의 값을 호출하는게 아닌 함수의 주소값을 전달하는 것입니다 → React에서 정한 문법이기 때문에 따라야합니다

 

📝HTML To JSX

아래 사이트에서 HTML을 JSX형식으로 변환시킬 수 있습니다

 

https://transform.tools/html-to-jsx

 

HTML to JSX

to TypeScript Declaration to TypeScript Declaration

transform.tools

 

반응형
반응형

📝리액트란

리액트는 Facebook에서 만든 Javascript 라이브러리핵심 요소가상 DOM으로 인한 빠른 렌더링 기술을 가진다

 

📝리액트 장점

  • 가상 돔을 이용한 빠른 렌더링
    • 가상돔을 이용해 필요한 부분만 렌더링해 속도의 최적화가 가능합니다
  • 클라이언트 사이드 렌더링
    • 클라이언트 측에서 렌더링하기 때문에 서버에 부담이 없어집니다
  • 컴포넌트 재활용
    • 클래스를 생성하고 객체를 만들어서 다양한 곳에 사용하는 것과 동일합니다 
    • 개인적인 의견 → 물론 사용해보니 여러명에서 개발할 때 공통 컴포넌트 자체를 만들기는 쉽지 않아보입니다 다양한 상황에는 다양한 조건들이 들어가는데 어떤 건 살짝만 다른데 코드가 더 들어가면서 코드가 덕지덕지 많아지며 점점 복잡해지니 많은 경험과 리팩토링도하면서 어떤 걸 공통으로 만들어야할지에 대한 개념을 익혀야합니다
  • React Native (크로스 플랫폼 개발)
    • React 문법을 익혀두면 React Native로 크로스 플랫폼 개발이 가능해 iOS, AOS 개발이 가능합니다
    • 개인적인 의견 →  현재 React Native의 버전이 아직도 1버전이 안 된 거보면 매우 불안정한 상태이고 공식적인 라이브러리도 따로 제공하지 않는 걸로 압니다 이 부분은 남이 만든 걸 가져다 쓰는데 업데이트가 주기적으로 이루어지지 않으면 운영하다 버그가 날 가능성이 높아보입니다
      현재 React도 비슷한 형태이지만 앱개발엔 많은 기술들이 들어가는데 React Native에서 기본적으로 제공하는 게 많이 없다면 npm에 의존할 수 밖에 없을 것 가텐요

 

📝리액트 단점

  • 러닝커브가 크다
    • 상태관리와 렌더링에 대한 개념이 어렵고 새롭기 때문에 기존 HTML과 Javascript로 개발하는 것과 다른 점이 있어서 난해할 수 있다
  • SEO 적용이 힘들다
    • 일반적으로 Server에서 그려져서 오면 검색엔진 봇이 내용을 읽어서 검색시 노출되게 할 수 있는데 React의 경우 Client Side Rendering이 주요 기술이라 Client가 제공해주는 js내용을 다운 받아서 그리기 때문에 검색엔진 봇이 들어왔을 때 아무 내용이 없어서 검색단어들을 가져갈 수 없는 것이죠 현재는 React에서도 Server Side를 지원하고 SEO 최적화를 위한 기술들을 제공하기 때문에 큰 문제가 되진 않아보입니다
  • 초기 로드시간이 느리다
    • CSR 방식을 이용하면 모든 스크립트와 리소스가 다운로드 될 때까지 기다려야 하기 때문에 첫 페이지 로드가 느릴 수 있습니다

 

📝리액트 동작과정 (Virtual Dom)

일반적으로 웹 브라우저의 동작은 웹 브라우저가 웹사이트의 텍스트 문서를 읽어서 DOM(Document Object Model)이라는 트리 구조로 바꾸어 사용자에게 보여주게 되는데, 웹 개발자들은 사용자의 반응에 따라 이 DOM 트리를 변경하여 웹사이트의 내용을 갱신합니다 (실제로 jQuery나 Vanila를 이용해 수정하게끔 이벤트를 만들어서 테스트하면 최상위 body의 값이 깜빡거리면서 전체를 렌더링하는 걸 확인할 수 있습니다) 하지만 매번 작은 변화가 있을 때마다 전체 화면을 새로 그리는 것은 성능에 좋지 않습니다

 

React는 이 문제를 해결하기 위해 Virtual DOM을 도입합니다 

Virtual Dom은 위 그림처럼 어느 부분이 변경 되어있는지 정확히 파악이 가능하기 때문에 변화를 주고 싶은 부분만 Virtual DOM에서 수정하게 됩니다

 

또한 DOM을 직접 조작하는 건 low-level이라 까다롭고 안정성이 떨어집니다 특히 규모가 커지면 엄청 많은 코드를 적어야할지도 모릅니다 그래서 돔으로 직접 조작하는 jQuery와 같은 라이브러리는 legacy 취급을 받습니다

 

📝Angular(앵귤러) vs Vue.js vs React

Angular, React, Vue 전부 비슷한 계열의 CSR이며 필요한 부분만 렌더링하는 기술들이 들어가있습니다

 

  • Angular
    • 구글에서 만든 프레임워크로서 구글에서 만들었지만 2년째 업데이트가 안 되고 있네요 구글에서는 자주 가져다 버리는 프로젝트들이 많습니다 어떤 기술을 택할 때 중요한 부분이니 고려 바랍니다
  • Vue
    • 1인 개발자가 만든 프레임워크인데 현재 다운로드수도 Angular에 비해 많고 React에 비해서 쉽다고 합니다

 

개인적으로 가장 많이 쓰면서 안정화된 버전이 나오고 최근에 업데이트되고 있는 기술을 쓰는게 좋습니다 고로 React를 사용하는게 좋다고 생각해요 물론 우리나라 취업시장을 고려하긴 해야합니다

 

🔗 참고 및 출처
https://velog.io/@juno7803/React%EA%B0%80-%ED%83%9C%EC%96%B4%EB%82%9C-%EB%B0%B0%EA%B2%BD

[React] React란? 동작원리는 어떻게 될까? (tistory.com)

https://modulabs.co.kr/blog/react-library/
https://npmtrends.com/angular-vs-react-vs-vue

반응형
반응형

const keywordEnrollOption: radio[] = [
  { value: Code.A, id: 'auto_enroll', label: '자동등록' },
  { value: Code.M, id: 'manual_enroll', label: '수동등록' },
];

export interface radio {
  value: string;
  id: string;
  label: string;
}
const RadioGroups = ({
  onValueChange,
  radios,
}: {
  onValueChange: RadioGroupContextValue['onValueChange'];
  radios: radio[];
}) => {
  return (
    <>
      <RadioGroup
        defaultValue={radios[0].value}
        className="flex"
        onValueChange={(value) => onValueChange(value)}
      >
        {radios.map((item) => (
          <div key={item.id} className="flex items-center space-x-2">
            <RadioGroupItem value={item.value} id={item.id} />
            <Label htmlFor={item.id}>{item.label}</Label>
          </div>
        ))}
      </RadioGroup>
    </>
  );
};

/** 컴포넌트 사용 **/
const dispatch = useAppDispatch();
const setKeywordStrategy = (value: string) => {
    dispatch(groupInfoSlice.actions.setKeywordStrategy(value));
};
  
<RadioGroups
    onValueChange={setKeywordStrategy}
    radios={keywordEnrollOption}
/>

RadioGroup 컴포넌트의 경우 https://ui.shadcn.com/에서 가져온 거라 그 부분은 본인이 만든 라디오 컴포넌트에 맞게 만드시면 됩니다

리덕스로 상태관리를 했으며 useState로 관리하는 것도 컴포넌트 사용에서 넘겨주는 Setter를 변경해서 넘겨주면 됩니다

반응형
반응형

export interface checkbox {
  text: string;
  code: string;
}

const weekOption: checkbox[] = [
  { text: '일', code: 'week1' },
  { text: '월', code: 'week2' },
  { text: '화', code: 'week3' },
  { text: '수', code: 'week4' },
  { text: '목', code: 'week5' },
  { text: '금', code: 'week6' },
  { text: '토', code: 'week7' },
];

const Checkboxes = ({
  checkboxOption,
  disabled,
}: {
  checkboxOption: checkbox[];
  disabled: boolean;
}) => {
  const [checkbox, setCheckbox] = useState<string[]>([]);

  const addCheckboxOption = (checked: CheckedState, value: string) => {
    let updatedOption: string[];

    if (checked) {
      updatedOption = [...checkbox, value];
    } else {
      updatedOption = checkbox.filter((el) => el !== value);
    }

    // checkboxOption에 적힌 순서대로 정렬 해서 추가
    const sortedCheckbox = checkboxOption.filter((checkbox) =>
      updatedOption.includes(checkbox.code),
    );

    setCheckbox(sortedCheckbox.map((checkbox) => checkbox.code));
  };

  return (
    <>
      {checkboxOption.map((checkbox) => (
        <React.Fragment key={checkbox.code}>
          <Checkbox
            id={checkbox.code}
            checked={disabled ? disabled : undefined}
            disabled={disabled}
            onCheckedChange={(e) => addCheckboxOption(e, checkbox.code)}
          />
          <label
            htmlFor={checkbox.code}
            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
          >
            {checkbox.text}
          </label>
        </React.Fragment>
      ))}
    </>
  );

Checkbox박스 컴포넌트의 경우 https://ui.shadcn.com/에서 가져온 거라 그 부분은 본인이 만든 체크박스 컴포넌트에 맞게 만드시면 됩니다

 


 

interface CheckBoxActions {
  [key: string]: ActionCreatorWithPayload<string[], string>;
}

const checkboxActions: CheckBoxActions = {
  groupWeek: groupInfoSlice.actions.setExposureWeek,
  groupExposureArea: groupInfoSlice.actions.setExposureArea,
};

type SelectorFunction = (state: RootState) => string[];

const stateSelectors: Record<string, SelectorFunction> = {
  groupWeek: (state) => state.groupInfo.exposureWeek,
  groupExposureArea: (state) => state.groupInfo.exposureArea,
};

const Checkboxes = ({
  checkboxOption,
  actionName,
  disabled,
}: {
  checkboxOption: checkbox[];
  actionName: string;
  disabled: boolean;
}) => {
  const dispatch = useAppDispatch();
  const action = checkboxActions[actionName];

  const checkbox = useSelector((state: RootState) => {
    const selector = stateSelectors[actionName];
    return selector ? selector(state) : [];
  });

  const addCheckboxOption = (checked: CheckedState, value: string) => {
    let updatedOption: string[];

    if (checked) {
      updatedOption = [...checkbox, value];
    } else {
      updatedOption = checkbox.filter((el) => el !== value);
    }

    // 정렬 추가
    const sortedCheckbox = checkboxOption.filter((checkbox) =>
      updatedOption.includes(checkbox.code),
    );

    const result = sortedCheckbox.map((checkbox) => checkbox.code);
    dispatch(action(result));
  };

  return (
    <>
      {checkboxOption.map((checkbox) => (
        <React.Fragment key={checkbox.code}>
          <Checkbox
            id={checkbox.code}
            checked={disabled ? disabled : undefined}
            disabled={disabled}
            onCheckedChange={(e) => addCheckboxOption(e, checkbox.code)}
          />
          <label
            htmlFor={checkbox.code}
            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
          >
            {checkbox.text}
          </label>
        </React.Fragment>
      ))}
    </>
  );

여기는 전역 상태관리 Redux를 사용한 경우 컴포넌트를 전역 상태관리할 수 있게 나름대로 코드를 만들어봤습니다

 

  • actionName 상태관리 변수명에 따라 useSelector로 값을 가져오며 상태관리 변수명에 따라 값을 Set할 수 있게 action을 설정해줍니다
  • checkboxActions, stateSelectors에서 actionName에 대한 "키"를 입력해주면 됩니다

 

반응형
반응형

백엔드는 프리즈마 ORM을 사용했고 커서기반으로 구현했습니다

 

Page

const ItemPage = () => {
  const TAKE = 10;
  const MEMBER_ID = 1; // 1 ~ 5
  const FIRST_CURSOR = 1;

  const { data, fetchNextPage, hasNextPage, isFetching } =
    useInfiniteFindItemsQuery(
      {
        memberId: MEMBER_ID,
        lastCursorId: FIRST_CURSOR,
      },
      {
        getNextPageParam: (lastPage, allPages) => {
          if (lastPage.findItems.length === 0) {
            return undefined;
          } else {
            return {
              memberId: MEMBER_ID,
              lastCursorId:
                lastPage.findItems[lastPage.findItems.length - 1]?.id + 1,
            };
          }
        },
        enabled: false,
        initialPageParam: {
          memberId: MEMBER_ID,
          lastCursorId: FIRST_CURSOR,
        },
      },
    );

  useEffect(() => {
    fetchNextPage();
  }, []);

  const observerRef = useIntersectionObserver({
    isFetching,
    hasNextPage,
    fetchNextPage,
  });

  return (
    <main className="mx-24 my-16">
      <SearchBar />
      {/*<Suspense fallback={ItemPage.Skeleton()}>*/}
      <div className="grid grid-cols-5 gap-x-14">
        {data?.pages.map((page, pageNum) =>
          page.findItems.map((item, index) => (
            <Item index={index + pageNum * TAKE} key={item.id} item={item} />
          )),
        )}
      </div>
      {/*</Suspense>*/}
      <div ref={observerRef} className="h-1" />
    </main>
  );
};

export default ItemPage;

 

 

최하단 엘리먼트 요소 감지 훅 및 다음 페이지 데이터 Fetch

import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  FetchNextPageOptions,
  InfiniteData,
  InfiniteQueryObserverResult,
} from '@tanstack/react-query';
import { IFindItemsQuery } from '@/gql/generated/graphql';

//hook props interface
interface IuseIntersectionObserverProps<TData = unknown> {
  isFetching: boolean | undefined;
  hasNextPage: boolean | undefined;
  fetchNextPage: (
    options?: FetchNextPageOptions,
  ) => Promise<
    InfiniteQueryObserverResult<InfiniteData<TData, unknown>, unknown>
  >;
}

export const useIntersectionObserver = <TData>({
  isFetching,
  hasNextPage,
  fetchNextPage,
}: IuseIntersectionObserverProps<TData>) => {
  const options = {
    root: null, // 감지할 대상 지정 null일 경우 뷰포트가 대상
    rootMargin: '0px', // root요소가 가장자리에 도달하는 즉시 교차 이벤트 감지
    threshold: 1.0, // 얼마나 많이 root요소와 교차하는지 설정 → 1.0 = 100% 완전히 겹친다
  };

  const handleIntersection = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      const target = entries[0];
      if (target.isIntersecting && hasNextPage && !isFetching) {
        fetchNextPage();
      }
    },
    [hasNextPage, isFetching, fetchNextPage],
  );

  const observerRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersection, options);
    if (observerRef.current) {
      observer.observe(observerRef.current);
    }
    return () => {
      observer.disconnect();
    };
  }, [handleIntersection, options]);

  return observerRef;
};

 

반응형