요즘 출퇴근 시간을 이용하여 리액트 공식문서를 다시 읽고 있다. 조무래기 시절 구현에 집중하여 리액트를 공부하다 보니, 작동원리나, 리액트에서 추구하는 방향에 대해서는 관심이 없었는데, 구현을 충분히 해본 뒤에 문서를 보니 아 내가 이렇게 몰랐나 싶었던 부분들이 많았다. 리액트에서는 왜 선언형으로 코드를 작성해야 할까부터 시작해서, key를 유용하게 사용하는 방법까지. 덕분에 리액트에 대한 깊이를 한층 더 깊게 가져가고 있다.
동시접속 기능
https://velog.io/@siwol406/동시-접속-로그아웃에-관한-고민과-수정 이번 스프린트에서는 기기제한 및 동시접속 기능을 구현해야 했다. 지속적인 fetching을 통해 확인해야 해서 socket을 생각했었다. 혹은 interval로 fetching을 구현하거나. 좀더 찾아보면서 sse같은 다양한 방식들이 있음을 알게되었다.
리액트 공식문서 뽀개기
리액트는 ui를 포현하는 프레임워크이다.
-
조건부로 JSX 반환하기
- 이 코드의 단점 : 중복코드가 발생하여 className을 변경할 경우, 두군데를 수정해야하는 유지보수의 어려움이 생긴다. → DRY하게 바꿔보자 (덜 반복적이게)
if (isPacked) { return <li className="item">{name} ✅</li>; } return <li className="item">{name}</li>;- 삼항연사자.
return <li className="item">{isPacked ? name + " ✅" : name}</li>; -
State 관리하기
-
각 컴포넌트는 독립된 state를 가진다 : 컴포넌트에 state를 줄때 state가 컴포넌트안에 살있는건 아님. state는 React 안에 있다.
-
React는 트리의 동일한 컴포넌트를 동일한 위치에 렌더링 하는 동안 상태를 유지. 컴포넌트를 제거하거나 다른 컴포넌트 렌더링시 React는 그 State를 버린다.
-
리렌더링 시 state를 유지하고 싶다면 트리구조가 같아야 한다.
-
→ 따라서 컴포넌트 함수를 중첩해서 정의하면 안된다.
-
반대로 같은 위치에서 state를 초기화 하고 싶다면?
-
다른 위치에 컴포넌트 렌더링 하기
{ isPlayerA && <Counter person="Taylor" />; } { !isPlayerA && <Counter person="Sarah" />; } -
key를 이용한 초기화 : Key는 배열만을 위한건 아님. 특히 form에서 용의
(key가 전역적으로 유일하진 않음. 오직 부모 안에서만 자리명시. )
- img 태그에서의 사용 :
<img>태그에key를 줄 수 있습니다.key가 바뀌면 React는<img>DOM 노드를 새로 다시 만듭니다. 이미지를 불러오는 동안 이미지가 잠깐 깜박이는데 앱의 모든 이미지가 이처럼 작동하기를 원하지는 않을 것입니다. 하지만 이미지와 설명이 항상 일치하도록 할 때는 일리가 있습니다.
{isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} //form <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} />번외3. 제거된 컴포넌트의 state 보존하기
내생각 : display none or state를 최상위로
- css 로 사용 : 간단한 ui에서 용이. 숨겨진 트리가 많을 경우 매우 느려짐.
- state 최상위 : 부모 컴포넌트에서 state관리. 가장 일반적인 방법
- 로컬 스토리지에 저장.
- 문제 : text가 리렌더 되는걸 어떻게 없앨 수 있을까?
import { useState } from "react"; //가장 좋은 방법은 같은 자리에서 렌더시키기 export default function App() { const [showHint, setShowHint] = useState(false); const [text, setText] = useState(""); //위로 올린다. return ( <div> {shoHint && ( <p> <i>Hint: Your favorite city?</i> </p> )} <Form /> <button onClick={() => { setShowHint(false); }} > Hide hint </button> </div> ); } function Form() { return ( <textarea value={text} onChange={(e) => setText(e.target.value)} /> ); } - img 태그에서의 사용 :
-
-
state와 reducer
- state 업데이트가 여러 이벤트 핸들러로 분산되는 경우. state를 업데이트하는 모든 로직을 reducer를 사용해 컴포넌트 외부로 단일함수로 통합해 관리.
- 컴포넌트 내부의 코드양을 줄일 수 있음.
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 },
];
-
이 코드의 단점 : state를 업데이트 하기 위해 setTasks를 호출. 컴포넌트가 커질수록 state를 다루는 로직의 양도 증가. 복잡성을 줄이고, 접근성을 높이기 위해서 컴포넌트 내부에 있는 컴포넌트 외부의 reducer라고 하는 단일함수로 옮기자.
-
state를 설정하는 것에서 action을 dispatch 함수로 전달하는 것으로 바꾸기.
- setTasks가 있는 로직을 모두 지워보면 add, change, delete 함수가 남음.
- 이벤트 핸들러에서 action을 전달하여 사용자가 한일 을 지정. state 업데이트 로직은 다른곳에 있음. → 이벤트 핸들러를 통해 tasks를 설적하는 것이 아닌 task 추가 변경 삭제 하는 action 전달. (사용자의 의도가 명확하게 전달됨)
function handleAddTask(text) { dispatch({ //action 객체 : 어떤 상황이 발생하는 지에 대한 최소한의 정보를 담고 있어야함. type: "added", //what happend 중요 id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: "changed", task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: "deleted", id: taskId, }); } -
reducer 함수 작성하기.
- reducer 함수는 state(
tasks)를 인자로 받고 있기 때문에, 이를 컴포넌트 외부에서 선언할 수 있습니다. 이렇게 하면 들여쓰기 수준이 줄어들고 코드를 더 쉽게 읽을 수 있습니다. - switch 을 사용하는것이 규칙. 가독성을 위해
function tasksReducer(tasks, action) { switch (action.type) { case "added": { return [ ...tasks, { //다음 state 반환 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); } } } - reducer 함수는 state(
-
컴포넌트에서 reducer 사용하기.
- useState부분을 useReducer로 바꿔주기
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
-
—> 결국에 무슨일이 일어났는지 명시만해주면됨. 그일이 어떻게 발생하는지는 몰라도됨.
useState vs useReducer
- 코드크기 : useReducer는 여러 이벤트 핸들러에서 비슷한 방식으로 state를 업데이트 하는 경우 코드양을 줄이는데 도움이 됨.
- 가독성 :
- 디버깅: 콘솔로그를 reducer에 추가하면 업데이트 되는 모든 부분을 확인할 수 있다.
- 테스팅:
- 개인적인 취향
state사용에서 버그가 자주 발생한다면 reducer사용을 권장.
useReducer 잘 작성하는법
- reducer는 순수해야합니다.
- immer로 간결하게 작성하기. useImmerReducer로 안전하게 state 변경
context를 사용하여 데이터를 깊게 전달하기
-
props drilling의 문제점
- 데이터를 깊이 전달하거나, 많은 컴포넌트에서 같은 prop이 필요한 경우 장황하고 불편할 수 있다.
-
context 사용하기
- context 생성하기
//컴포넌트에서 사용할 수 있도록 export해야함. export const LevelContext = createContext(1); //유일한 인자는 기본값-
데이터가 필요한 컴포넌트에서 context 사용
export default function Heading({ children }) { const level = useContext(LevelContext); // ... } -
데이터를 지정하는 컴포넌트에서 context 제공
export default function Section({ level, children }) { return ( <section className="section"> <LevelContext.Provider value={level}>{children}</LevelContext.Provider> </section> ); }
최종
import Heading from "./Heading.js";
import Section from "./Section.js";
export default function Page() {
return (
<Section level={1}>
<Heading>Title</Heading>
<Section level={2}>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Section level={3}>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
- context를 사용하기 전에 고려해야 할것
- props 전달하기
- children으로 전달하기
- context가 유용한 예시
- 테마지정. 다크모드
- 현재 계정.
- 라우팅
- 상태관리
Reducer와 Context로 앱 확장하기
https://velog.io/@superlipbalm/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior
리액트 렌더링 동작에 관한 완벽한 가이드
렌더링 프로세스 개요.
자바스크립트가 컴파일되고, 배포를 위해 준비될때 react.createElement() 호출로 변환.
createElement : 의도된 UI의 구조를 설명하는 일반 자바스크립트 객체인 React요소를 반환.
// JSX 구문입니다.
return <MyComponent a={42} b="testing">Text here</MyComponent>
// 아래와 같은 호출로 변환됩니다.
return React.createElement(MyComponent, {a: 42, b: "testing"}, "Text Here")
// 그리고 이것은 다음과 같은 요소 객체가 됩니다.
{type: MyComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}
reconciliation 재조정 : 컴포넌트 트리 전체에서 렌더출력을 수집. 새로운 객체트리(가상돔)과 비교해 변경사항 목록을 수집. 계산된 변경사항을 하나의 동기 시퀸스로 dom에 적용.
렌더 및 커밋 단계
렌더페이즈 : 컴포넌트를 렌더링. 변경사항 계산.
커밋페이지 : 렌더단계에서 계산된 변경 상항을 dom에 적용.
그다음 componentdidmount, componentdidupdate,useLayoutEffect훅을 동기적으로 실행.
제한 시간을 설정하고
시간 반료시 useEffect훅 실행. passive effect
usetransition 동시렌더링 기능 : 브라우저가 이벤트를 처리할 수 있도록 렌더링 단계에서 작업을 일시중지 가능.
렌더링 ≠ dom update : 어떠한 가시적인 변경도 일어나지 않고 컴포넌트가 렌더링 될 수 있다.