1. React Hooks?
리액트의 컴포넌트는 클래스, 함수형 2가지 방식으로 만들 수 있다.
이전에는 클래스 컴포넌트를 많이 사용했는데, 그 이유는 컴포넌트의 상태와 생명주기를 클래스에서만 다룰 수 있었기 때문이다.
(그럼 함수형에서는 뭘 할 수 있었냐? UI를 렌더링 하는 역할이었다.)
어차피 Hooks가 나오게 된 배경을 설명하느니라 나온 거라 생명주기가 무엇인지 강의에서 잘 다뤄지지 않아 서치해보았다.
그냥 이 부분은 그렇구나 하고 이해하고 넘어갔는데 되게 복잡했다.
- Mounting(생성) : 컴포넌트가 생성될 때 발생하는 단계
- constructor() : 컴포넌트의 인스턴스를 생성하고 초기화 한다.
- render() : 컴포넌트의 UI를 렌더링 한다.
- componentDidMount() : 컴포넌트가 DOM에 삽입된 후 호출된다.
- Updating(갱신) : 컴포넌트가 업데이트 될 때 발생하는 단계로 props나 state가 변경되었을 때 발생한다.
- static getDerivedStateFromProps(props, state) : props에 따라 state를 업데이트 한다.
- shoulComponentUpdate(nextProps, nextState) : 컴포넌트가 리렌더링을 해야 하는지 결정한다.
- render() : 컴포넌트의 UI를 렌더링 한다.
- componentDidUpate(prevProps, prevState) : 컴포넌트의 업데이트가 완료된 후 호출된다
- Unmounting(제거) : 컴포넌트가 제거될 때 발생하는 단계
- componentWillUnmount() : 컴포넌트가 DOM에서 제거되기 직전에 호출되며 이벤트 리스너의 제거 등을 수행한다.
암튼 이런 생명주기 메서드를 Hooks이 나오기 이전에는 클래스형 컴포넌트에서만 사용할 수 있었다.
하지만 클래스 컴포넌트 코드가 더 많고, 더 복잡하고, 더디다는 단점이 있었다.
저 메서드를 사용하기 위해 클래스 컴포넌트를 사용하기에는 불편함이 더 컸던 상황에서 Hooks가 등장했다.
이 녀석 덕분에 함수형 컴포넌트에서도 생명주기 메서드를 사용할 수 있게 되었다. 오히려 더 간편하게 처리할 수 있게 됐다.
(useState, useEffect 등 노마드 코더에서 잠깐 배웠던 것들도 Hooks을 사용한 것이었다!)
어떤 훅이 있는지 간단하게 또 검색해보았다.
- useState : 상태 관리. 상태 값을 저장하고 업데이트 할 수 있다. (버튼의 클릭 여부나 폼 입력값 저장 등)
- useEffect : 이벤트 처리, 데이터 요청 등 부작용을 처리할 수 있다. 함수형 컴포넌트가 렌더링 될 때마다 실행되거나 특정 상태나 props가 변경될 때 실행하도록 설정할 수 있다.
- useContext : 컨텍스트 값을 읽어올 수 있다. 컨텍스트란 상위 컴포넌트에 전달한 값을 하위 컴포넌트에서도 사용할 수 있게 한다.
- useMemo : 렌더링을 최적화 한다. 계산 비용이 큰 연산 결과를 캐싱(저장한다는 건가?)하여 성능을 향상 시킬 수 있다.
- 사용자 정의 훅: 개발자가 필요에 따라 자신의 커스텀 훅을 만들 수 있다. 로직을 추상화하고 재사용성을 높인다.
- HOC 컴포넌트 (Higher Order Component: 화면에서 재사용 가능한 로직만 분리해 컴포넌트로 만들고 재사용 불가한 UI 같은 부분은 파라미터로 받아서 처리하는 방법) 를 사용자 정의 훅으로 대체해서 많은 Wrapper 컴포넌트를 줄일 수 있다.
- 예를 들어, A 페이지와 B 페이지가 있을 때 두 페이지 모두 같은 소스를 사용하는 부분(유저 리스트를 가져오는 부분)이 있다고 해보자. 두 페이지 모두에서 유저 리스트를 가져오기 위해 똑같은 소스를 넣어준다면 각 페이지에 똑같은 소스 상당 부분이 중복된다. 따라서 유저 리스트를 가져오는 공통적인 부분을 HOC 컴포넌트에 넣어주고 그 컴포넌트로 각각의 컴포넌트를 감싸주는 것이다.
- Hooks가 나오기 전에는 이 방법이 추천되었으나 이렇게 하면 너무 많은 감쌈의 감쌈의 감쌈의... 감쌈이 이어지는 것이다.
- HOC 컴포넌트 (Higher Order Component: 화면에서 재사용 가능한 로직만 분리해 컴포넌트로 만들고 재사용 불가한 UI 같은 부분은 파라미터로 받아서 처리하는 방법) 를 사용자 정의 훅으로 대체해서 많은 Wrapper 컴포넌트를 줄일 수 있다.
2. 이전에 만들었던 to do 앱을 함수형 컴포넌트로 변경해보자!
변경할 때 정해진 순서는 없지만, 컴포넌트 자체를 바꾸고 render() 를 삭제하고 return() 으로 변경해준다.
이 외에도 setState로 되어 있던 부분을 useState Hook으로 정의하기 위해 const [value, setValue] = useState("") 와 같은 형태로 수정해준다.
클래스로 사용하면서 썼던 this 부분도 변경해주면 되는데 (아 이 부분 클래스며 this를 잘 모르니 이해는 했는데 설명이 안된다.)
그외 클래스로 선언된 부분들은 모두 상수 선언을 해주면 뚝딱 완성!
...말이 쉽지 강의 따라가면서 눈알 빠지는 줄 알았다!
3. State와 Props
state와 props 모두 리액트에서 컴포넌트간 데이터를 전달하고 관리하기 위해 사용되는 개념이다.
State
컴포넌트 안에서 관리되는 데이터.
컴포넌트가 렌더링 될 떄 변하는 값으로 뭐 인풋 박스에 타이핑 할 때 그대로 변하는 것은 State를 바꾼 것이다.
state는 mutable하며, setState 또는 Setter를 사용해서 변경할 수 있고, 이 상태가 변하면 리렌더링 된다.
Setter라고 쓰니까 굉장히 어려워 보이는데... Setter는 state를 업데이트 하는 함수를 가리킨다.
useState에서 쓰는, 저 위에 보라색으로 볼드 처리한 [value, setValue] = useState("") 의 setValue가 setter인 것이다.
Props
프로펄티의 줄임말로 상속하는 부모 컴포넌트로부터 자녀 컴포넌트에 데이터를 전달하는 방법이다.
props는 읽기 전용이라 자녀 컴포넌트 입장에서는 변하지 않고, 변경하려면 부모 컴포넌트의 state를 변경해야 한다.
뭔가 설명으로 쓰니까 좀 어려워 보이는데...
결국 state는 상태고 props는 부모 컴포넌트에 있는 데이터를 아래로 내려주는 방법인 것이다.
4. to do 앱을 여러 컴포넌트로 나누어 담자
어제 올린 코드를 보면 to do 앱이 App.js 하나의 컴포넌트에 모두 들어가 있다.
App 컴포넌트 - 리스트 컴포넌트 - Form 컴포넌트 3가지로 부분을 나누어 보자.
컴포넌트를 분리할 때는 정답이 없고 재사용률을 고려하면 된다.
tip. 함수형 컨포넌트를 생성할 때 익스텐션 ES7을 깔아서 사용하면 편리하다.
먼저 Lists.js를 생성한 후 App.js에 이 부분을 import 해주어야 이어진다.
UI 부분을 먼저 가져온 후 UI를 구현할 때 필요한 State, 함수를 가져온다.
App.js에 있어야 하면서 Lists에도 필요한 부분은 prop로 내려준다.
<Lists
handleClick={handleClick}
todoData={todoData}
setTodoDate={setTodoDate}
/>
handleClick은 함수형 prop로 Lists 컴포넌트 내부에서 사용될 클릭 핸들러 함수를 가리킨다.
todoData는 데이터 prop으로, 배열 형태로 전달된다.
setTodoData는 상태 업데이트 함수 prop으로, Lists 컴포넌트에서 호출되면 일정 데이터를 바꾸고, 다시 렌더링 되도록 한다.
결론은 이런 식으로 prop를 내려준다는 말이다.
5. 구조 분해 할당
배열이나 객체의 속성을 해체해서 그 값을 개별 변수에 담는 JS의 표현식을 말한다.
이 놈을 사용하는 이유는 클린 코드를 위해서다.
function buildAnimal(animalData) {
let accessory = animalData.accessory,
animal = animalData.animal,
color = animalData.color,
hairType = animalData.hairType}
function buildAnimal(animalData) {
let {accessory, animal, color, hairType} = animalData;
}
첫 번째보다 두 번째의 코드가 훨씬 짧고 간결하다.
공식 문서에도 다양한 예시가 있다.
구조 분해 할당 - JavaScript | MDN
구조 분해 할당 구문은 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식입니다.
developer.mozilla.org
나머지 연산자를 사용해서 나머지 요소들을 가져올 수도 있다.
나머지 매개변수 - JavaScript | MDN
나머지 매개변수 구문을 사용하면 함수가 정해지지 않은 수의 매개변수를 배열로 받을 수 있습니다. JavaScript에서 가변항 함수를 표현할 때 사용합니다.
developer.mozilla.org
const numbers = [1, 2, 3, 4, 5];
// 배열 분해 할당과 나머지 연산자
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
객체 분해 할당은 객체의 각 속성을 개별 변수로 분해해서 할당하는 것이다.
아래 예시를 보자.
const person = {
name: 'John',
age: 30,
city: 'New York'
};
// 객체 분해 할당
const { name, age, city } = person;
console.log(name); // 'John'
console.log(age); // 30
console.log(city); // 'New York'
객체를 이렇게 찢어놨다.
객체 분해 할당에서 변수의 이름과 객체의 속성이 일치할 필요는 없다.
아래와 같이 변수의 이름을 변경하여 할당할 수도 있다.
const person = {
name: 'John',
age: 30,
city: 'New York'
};
// 객체 분해 할당과 변수 이름 변경
const { name: personName, age: personAge, city: personCity } = person;
console.log(personName); // 'John'
console.log(personAge); // 30
console.log(personCity); // 'New York'
6. TailWindCSS와 react-beautiful-dnd을 써보자 (⭐️⭐️⭐️)
CSS 프레임워크인 TailWindCSS를 사용해보았다.
부트스트랩보다 훨씬 가독성 좋고 편리해서 앞으로 CSS 프레임워크는 이걸 사용할 예정이다.
어떤 점이 더 좋냐면, IntelliSence 플러그인의 도움을 받아서 어떤 효과에 대한 설명이 나와 있고, 색의 경우에도 미리 보여준다.
메모 앱에서 드래그 앤 드랍 기능을 구현하기 위해 react-beautiful-dnd라는 라이브러리를 사용해보았다. (어렵다...)
아무래도 강의를 들으면서 라이브러리에 대해 하나도 모른 채 따라가다 보니 사용법을 제대로 숙지하지 못했다.
<DragDropContext />는 드래그 앤 드랍을 하고 싶은 부분을 감싸준다.
<Draggable />은 드랍할 수 있는 공간을 의미하며 <Draggable />은 드래그할 수 있는 것을 의미한다.
자세한 사용 설명은 깃허브나 다른 분들의 글을 참고했다. 한국어의 위대함에 오늘도 기절.
GitHub - LeeHyungGeun/react-beautiful-dnd-kr: react-beautiful-dnd's Korean document.
react-beautiful-dnd's Korean document. Contribute to LeeHyungGeun/react-beautiful-dnd-kr development by creating an account on GitHub.
github.com
react-beautiful-dnd로 TODO 뽀개기
라이브러리를 활용해서 React에서 Drag and Drop TODO 칸반 보드를 구현해보자!!!
bepyan.github.io
빠른 시일 내에 혼자 복습할 필요성 718%
또 하나 팁이 있다면 React18에서는 StrictMode를 삭제하고 사용해야 한다.
7. 리액트의 불변성
불변성이라고 하면 '변하지 않는 것'이라는 사전적인 의미가 있지만, React에서는 데이터의 변경 불가능성이라고 봐야 할 것 같다.
자료형을 원시 타입(boolean, string, number, null, undefined, symbol)과 참조 타입(array, object)으로 나눌 수 있다.
원시 타입은 데이터를 직접 저장하는 데이터 타입이다. 그래서 값이 변수에 직접 저장되며 값이 복사된다.
참조 타입은 데이터가 메모리에 할당된 위치(참조)를 가리키는 데이터 타입이다.
그래서 참조 타입의 값은 객체나 배열이 저장된 메모리 주소를 가리키는 참조 값으로 표시된다.
무슨 말이냐면, 원시 타입과 달리 변수에 할당되는 것은 실제 데이터가 아닌 참조 값(위치)인 것이다.
원시 타입은 불변성을 가지고 있고, 참조 타입은 그렇지 않다.
갑자기 자료구조 데이터로 넘어왔는데... 🤯
JS에서는 원시 타입에 대한 참조 및 값을 저장하기 위해 콜 스택 메모리 공간을 사용하지만,
참조 타입의 경우에는 Heap이라는 별도의 메모리 공간을 사용한다.
이 경우에 콜 스택은 개체나 배열 값이 아닌 메모리에만 Heap 메모리 참조 ID를 값으로 저장한다.
콜 스택에 대해 간단히 알아본 바로는 실행중인 함수의 호출 정보를 기록하는 자료구조다.
함수가 호출 될때마다 해당 함수의 호출 정보가 스택에 쌓이고, 함수가 반환되면 그 호출 정보가 스택에서 제거된다.
콜스택은 stack처럼 LIFO 방식을 동작한다. 저 그림처럼 어떤 변수에 값이 매치되어 저장되어 있다.
힙은 동적으로 할당된 메모리 영역을 가리킨다. (무슨 말이냐...)
객체와 배열 같은 참조 타입의 데이터가 저장되는 공간이라고 우선 이해했다.
원시 타입이 실제 데이터를 변수에 할당했던 것과 달리, 참조 타입은 변수에 이 힙의 메모리 주소 값을 할당한다.
그래서 이게 리액트랑 뭔 상관이냐면... (ㅠㅠ)
let username = 'walter'
username = 'john'
원시 타입의 경우 john으로 대체한 것이 아니라, 메모리 영역 a에 있을 walter 값은 그대로 두고 메모리 영역 b에 john을 새로 할당한다.
값을 대체하는 것이 아니라 새로운 메모리 영역을 늘려 값을 추가한 것이다.
let array = ['1', '2', '3']
array = ['4', '5', '6']
참조 타입의 경우 배열에 대한 요소를 추가하거나 객체 속성의 값을 변경할 때,
아까 말했던 콜 스택의 참조 id는 동일하게 유지되나 heap 메모리에서는 값이 변경된다.
Call Stack Heap
+---------+ +--------------+
| obj | ----> | { name: |
+---------+ | "John" } |
+--------------+
chatGPT가 손수 그림까지 그려주었다.
콜 스택에 저장된 obj 변수는 실제 객체 데이터가 저장된 heap의 메모리 주소를 가리키는 '참조값'만 갖고 있다.
heap에 있는 진짜 데이터가 수정되기 때문에 참조 타입은 불변성이 없다고 하는 것이다.
리액트에서 왜 불변성이 중요한가?
리액트에서 참조 타입의 값이 변하면 원본 데이터 역시 변경되어 이 원본 데이터를 참조하고 있는 다른 객체에서 오류가 날 수 있다.
또 이전에 말한 것처럼 리액트는 이전 값과 비교해서 변경된 사항을 확인 후 업데이트 하기 때문에 불변성이 지켜져야 한다.
리액트에서 불변성을 지키는 방법은 엄청 대단한 건 아니고 그냥 원본을 건드리지 않고 새로운 배열을 반환하는 메서드를 사용하면 된다.
원본 배열을 수정하는 splice나 push 같은 게 아니라 새로운 배열을 반환하는 map, slice, 전개 구문 등을 사용하면 되는 것이다!
결국 직접 원본 데이터를 수정하는 게 아니라 다른 함수를 통해 (거의 반환) 수정하라는 말을 이렇게 자료구조와 엮어 이해했다.
8. List 컴포넌트 생성하기 && React.memo
List.js 파일을 생성했다. Lists.js를 생성해 컴포넌트를 쪼갰던 것과 동일하게 진행하면 된다. (UI, 함수, props 등을 내린다.)
현재 App, List, Lists, form 4가지 컴포넌트로 나누었다.
form에서 to do를 입력하기 위해 타이핑을 할 때에는 form 컴포넌트와 그 State 값을 가진 App 컴포넌트만 렌더링이 되어야 한다.
하지만 현재 모든 컴포넌트에서 렌더링이 이뤄지고 있다.
니꼬 강의에서도 들었던 부분인데, 부모 컴포넌트와 자식 컴포넌트가 여러 개 있을 때, 특정 자식 컴포넌트만 리렌더링이 필요하고 나머지는 필요하지 않을 경우 React.memo()를 사용하면 나머지 자식들은 원래의 값을 기억하고, 같다면 리렌더링을 하지 않는다.
이게 왜 필요하냐면, 만약 부모 컴포넌트 아래 자식 컴포넌트가 1억개라고 했을 때 내가 리렌더링 하고 싶은 놈은 1개 뿐인데 모든 게 리렌더링 되면 어플리케이션이 매우 매우 느려질 수 있기 때문이다.
쓰는 방법은 매우 간단하게 React.memo()로 감싸주면 된다.
9. useCallback을 이용한 함수 최적화
리액트 훅 중 하나인 useCallback은 함수를 메모이제이션하는 기능을 제공한다.
메모이제이션은 react.memo()에서 그랬던 것처럼 동일한 입력에 대해 결과를 캐싱해서 성능을 향상 시키는 방법이다.
이 훅을 쓰는 이유는 자식 컴포넌트에 전달되는 함수들이 자식 컴포넌트가 리렌더링 될 때마다 새로 생기는 것을 방지하기 위함이다.
const memoizedCallback = useCallback(callback, dependencies);
콜백 함수와 콜백 함수가 의존하는 값의 배열을 인자로 사용한다.
디펜던시에 아무 값도 없다면 컴포넌트가 처음 렌더링 했을 때만 함수가 생성되고, 이후에는 동일한 함수를 유지한다.
const handleClick = useCallback(
(id) => {
let newTodoData = todoData.filter((data) => data.id !== id);
setTodoDate(newTodoData);
},
[todoData]
);
이런 식으로 todoData가 바뀔 때만 함수가 다시 돌아간다.
10. 할 일 모두 지우기 버튼 생성
다른 거 보다가 이 버튼 생성하기 하니까 선녀 같네...
메모 앱 UI에 삭제하기 버튼을 누르고 이벤트를 선언해 setTodoDate를 빈 배열로 바꿔준다.
<button onClick={handleRemoveClick}> Delete all </button>
const handleRemoveClick = () => {
setTodoDate([]);
};
11. 할 일 수정하기 기능 추가
기본 원리는 edit 을 누르면 수정하고 save를 누르면 저장하는 것이다.
먼저 UI를 위해 state를 생성한다.
const [isEdting, setIsEditing] = useState(false);
const [editedTitle, setEditedTitle] = useState(title);
수정 버튼을 누르면 setIsEditing(true)가 되고, isEditing이 true가 되면 아래와 같이 input 부분이 수정되는 조건문을 달아준다.
<input
value={editedTitle}
onChange={handleEditChange}
className="w-full px-3 py-2 mr-4 text-gray-500 rounded"
/>
저장 버튼을 누를 때 버튼에 onClick, 전체를 감싸고 있는 폼에 onSubmit을 걸어 handleSubmit이라는 함수를 작동 시킨다.
const handleSubmit = (event) => {
event.preventDefault();
let newTodoData = todoData.map((data) => {
if (data.id === id) {
data.title = editedTitle;
}
return data;
});
setTodoDate(newTodoData);
setIsEditing(false);
};
수정하는 데이터의 id가 일치한다면 title도 수정된 타이틀로 변경한 후 데이터를 반환한다.
새로운 newTodoData를 setTodoData로 리렌더링 시키면서 에디팅 불리언 값도 false로 다시 돌려준다.
불리언 값을 잘 활용해서 기능을 구현해봐야겠다.
12. localStorage에 저장하기
바닐라JS로 크롬 앱을 만들 때에 사용했던 로컬 스토리지다.
로컬 스토리지에 값을 저장할 때는 setItem(키, 값)을 사용해야 하며, 객체나 배열을 저장할 때는 JSON.stringfy를 사용해 텍스트로 변환해주어야 한다.
todoData state를 변경할 때 아래와 같이 코드를 추가해준다.
localStorage.setItem("todoData", JSON.stringify(newTodoData));
저장된 값을 가져올 때는 getItem을 사용하고, 이 때는 다시 객체나 배열 형태로 돌려주기 위해 JSON.parse를 사용해준다.
로컬 스토리지에 todoData 의 값이 있다면 객체나 배열 형태로 가져오고, 그렇지 않으면 빈 배열을 유지하도록 작성해준다.
const initialTodoData = localStorage.getItem("todoData")
? JSON.parse(localStorage.getItem("todoData"))
: [];
완성된 to do 앱!
드래그 앤 드랍에서 하나도 못 알아 들어서 이 부분이 특히 어려웠다.
내일은 이렇게까진 안되어도 다시 만들어보도록 하자... (할 수 있을까... ㅠㅠ)
'⚛️ React > 💭 따라하며 배우는 React A-Z' 카테고리의 다른 글
[React] 넷플릭스 웹페이지의 모달을 구현해보자 (feat. 따라하며 배우는 리액트 A-Z) (0) | 2023.06.02 |
---|---|
[React] 넷플릭스 웹페이지의 영화를 나열하고 footer를 만들자 (feat. 따라하며 배우는 리액트 A-Z) (0) | 2023.06.02 |
[React] 넷플릭스 웹페이지의 배너를 클론해보자 (feat. 따라하며 배우는 리액트 A-Z) (0) | 2023.05.30 |
[React] 리액트로 to do 앱을 만들어보자 (feat. 따라하며 배우는 리액트 A-Z) (0) | 2023.05.24 |
[React] 리액트에 대해 알아보자 (feat. 따라하며 배우는 리액트 A-Z) (0) | 2023.05.24 |