리액트

한 입 크기~ 리액트 todo

자무카 2024. 6. 17.

1. 구현 조건

  • 구현 화면은 위와 같음
  • Todo 추가/체크/삭제/검색 기능

 

2. UI 구현

component 나누기 (Header, Editor, List/TodoItem )

  • 3개의 component로 나눔
    • Header
    • Editor
    • List

기본 레이아웃 설정

App.css

box-size: border-box;

margin: 0 auto; 가운데 정렬

display: flex; flex-direction: column;

.App {
  border: 1px solid gray;
  box-sizing: border-box;
  max-width: 500px;
  width: 100%;
  margin: 0 auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap:30px;
}

display: flex 한 줄로 가로 배치(기본값)인데, flex 속성 이용하면서, 세로(column) 로 배치. 반응형 구현.
margin : 0 auto;
위 아래에는 값 0. 좌우 auto균등, 중앙 위치.

Header.jsx

Editor.jsx

  • input 태그는 flex: 1로 둬서 부모의 범위를 넘어가지 않는 선에서 최대로 확장되게 구현

List

  • 서치바는 width: 100%를 줘서 부모에 가득차게 구현
  • 서치바에 padding: 10px 0을 줘서 상하에만 padding 구현
  • border-bottom을 기본, focus일 때 구현
  • List의 투두리스트는 반복되므로 TodoItem component로 뺌

 

기능 구현

기능 구현 준비

구현할 기능

  • Todo 추가/체크/삭제/검색 기능

위 기능을 구현하기 위해서는 todoItem들의 데이터를 State로 만들어서 보관해야 한다. 왜냐하면 새로운 데이터가 추가/수정/삭제되었을 때 화면에 변화를 반영할 수 있기 때문이다. State는 모든 자식 component에 전달해야하므로 조상 component인 app component에 만들어야 한다.

테스트 데이터로 각각의 아이템을 구별하는 id, 체크박스 유무를 나타내는 isDone, todo 컨텐츠를 보관할 content, 아이템이 언제 생성되었는 지를 알려주는 date 3가지 데이터를 mockData로 만든다. 그리고 useState를 mockData로 초기화 시켜준다.

 

TodoList - Create(투두 추가하기)

추가하기 기능

추가버튼을 누르면 원하는 컨텐츠를 추가하는 기능을 하기 위해, onCreate 함수를 만들어준다. onCreate 함수는 content를 매개변수로 받아서 newTodo라는 객체의 content 값에 넣어준다.

todos의 값은 setTodos로 바꿔줘야하기 때문에 onCreate 함수 마지막에 setTodos 함수 안에 생성한 newTodo와 스프레드 연산자로 ...todos를 넣어주면 된다.

주의해야할 점은 todos.push(newTodo)이런 식으로는 절대 하면 안된다. 왜냐하면 todos라는 것은 상태여서 반드시 상태함수 setTodos로 값을 변경해야 리렌더링 되기 때문이다.

다음은 버튼을 클릭했을 때 생성한 onCreate 함수를 Editor 컴포넌트에 인수로 전달해준다.

입력받은 input 태그의 값을 저장할 State인 content와 입력값을 감지할 onChangeContent도 만들어준다. onSubmit를 만들어 onCreate 함수를 부르고 매개변수로 content를 전달한다. 버튼의 onClick 이벤트가 발생하면 onSubmit을 불러준다.

추가 버튼을 클릭하면 위와 같이 앞에 객체가 추가된 것을 볼 수 있다.

그리고 id값은 주민등록번호처럼 고유해야하는데, 지금은 0으로 동일하다. useRef를 이용해서 idRef를 만들어준다.

id의 값을 받아올 때 현재값 +1로 지정해주면 다음과 같이 id값을 저장할 수 있다.

비었을 때 추가하지 않는 기능 + 포커스 기능

onSubmit 함수 안에서 content값이 비어있으면 return 하게 해주면 된다.

contentRef를 하나 만들어주고, input 태그안에 ref로 지정해준다. 그리고 content가 비어있을 때 contentRef.current.focus()를 지정해주면 값이 비어있을 때 포커스해준다.

추가되면 입력창을 비워주는 기능

![](https://velog.velcdn.com/images/junnkyuu/post/4f614e64-920f-4861-9b4 1-b89abe451957/image.png)

간단하게 추가되면 setContent("")를 추가해서 content값을 빈 문자열로 바꿔주면 된다.

엔터키를 눌렀을 때 추가되는 기능

onKeyDown이라는 이벤트 핸들러를 만들어주고, input 태그의 사용자가 키보드를 입력했을 때 발생하는 이벤트를 담당하는 onKeyDown에 연결해주면 된다. 우리가 작성한 onKeyDown 함수 안에서는 사용자가 엔터키를 눌렀을 때 입력이 되야하므로 조건문안에 e.keyCode == 13일 때 onSubmit()을 호출해주면 된다.

 

todo-list 렌더링

todos를 List component의 props로 전달하고, List component에서 받아온 todos를 map함수를 사용해서 content를 출력해준다.

위와 같이 저장된 todos의 content가 출력된다.

미리 만들어놓은 TodoItem component를 사용해서 저장된 todos의 데이터를 가져오려면, props로 ...todos를 전달한 후 TodoItem에서 받아서 사용하면 된다.

다음과 같이 출력된다.

콘솔창을 열어보면 다음과 같은 오류가 발생하는데, 이는 각각을 구분할 수 있는 key를 지정하지 않아서 그렇다.

해결하려면 key값으로 todo.id값도 넘겨주면 된다.

 

검색 기능

검색을 하면 리렌더링 되어야 하므로 검색도 state로 저장해야한다. 그리고 입력값을 받을 때마다 리렌더링 되어야하므로 onChangeSearch 함수를 작성해준다.

그리고 받은 검색을 찾을 getFilteredData 함수를 작성해준다. 함수는 받아온 search값을 todos에 있으면 반환해주는 역할을 한다.

toLowerCase로 모두 소문자로 변환해서 필터링해주면 다음과 같이 검색 기능을 구현할 수 있다.

 

TodoList - Update(투두 수정하기)

App.jsx에서 onUpdate 함수를 작성해서 todos State의 값들 중에 targetId와 일치하는 id를 갖는 todo item의 isDone을 바꿔주면 된다.

List에 onUpdate를 전달해주고, 그것을 또 TodoItem으로 onUpdate를 전달해준다. TodoItem에서 onChangeCheckBox 함수를 만들어줘서 받은 onUpdate함수를 불러주면, 체크박스를 클릭할 때마다 isDone값이 바뀐다.

 

TodoList - Delete(투두 삭제하기)

onDelete함수를 App.jsx에서 만들어서 todo 배열에서 targetId와 같은 id를 가진 todo만 삭제해서 새로 만들어서 반환해주면 된다.

onDelete 함수를 props로 List, TodoItem까지 전달한다. 그리고 TodoItem에서 onClickDeleteButton를 만들어서 onDelete를 불러주고, button에 onClick이벤트로 달아주면 된다.

 

useReducer

useReducer, useState?

useReducer는 component 내부에 새로운 State를 생성하는 React Hook이다.모든 useState는 useReducer로 대체할 수 있다. useState와의 차이점은 상태 관리 코드를 component 외부로 분리할 수 있다.

useState를 사용하면 내부에 상태 관리 코드를 작성해야한다.

useReducer를 사용하면 State 생성만 해놓고, reducer라는 함수를 통해 외부에서 상태 관리 코드를 작성할 수 있다.

기존의 코드를 보면 useState를 사용해서 반드시 상태 관리하는 코드 onCreate, onUpdate, onDelete들이 App Component 내부에 있어야 했다. 왜냐하면 State에 접근하는 setTodos함수는 App component 외부에서 접근할 수 없기 때문이다.

지금은 상태 관리 코드가 짧아서 괜찮은데, 훨씬 더 복잡한 상태 관리를 하거나, 코드가 길어질 경우 유지보수와 가독성이 떨어질 수 있다.

component의 주된 목적인 UI를 렌더링하는 것인데, 상태 관리 코드가 길어지게 되면 주객이 전도된 것이다.

따라서 component 외부에 상태 관리 코드를 분리하기 위해 useReducer를 이용한다.

 

useReducer 실습

Exam이라는 카운트 앱을 하나 만들어주고 useReducer를 import해서 state와 dispatch를 작성해준다.

dispatch 함수는 상태 변화를 요청하기만 하는 함수다.

component 내부에서 dispatch 함수를 호출하게 되면 상태변화가 요청이 되고, 그러면 useReducer가 상태 변화를 실제로 처리하게 될 함수를 호출하게 된다.

변환기라는 뜻을 가진 reducer 함수를 Exam component 밖에 만들어주고, useReducer 첫번째 매개변수로 reducer 함수를 넣어준다.

reducer 함수가 상태를 실제로 변화시키는 함수이다.

useReducer 함수의 두번째 매개변수로는 초기값을 전달한다.

h1 태그 안에 state 값을 렌더링하게 해주면 된다.

버튼을 클릭했을 때, dispatch 함수를 통해 상태변화를 요청하면 된다.

버튼을 클릭했을 때 연결할 onClickPlus 함수를 만들어준다. 내부에 dispatch를 호출하게 만들고, 매개변수로는 어떻게 변화시킬지에 대한 type을 increase, 얼마나 데이터를 변화시킬 것인지에 대한 data를 1만큼 객체형태로 준다. 이것을 action 객체라고 한다.

외부의 reducer 함수에 첫번째 인자로 state, 두번째 인자로는 action을 주고 console.log를 찍어보면 다음과 같다.

useReducer에서 새로운 state 값을 반환해주면 state의 값을 바꿔준다. 조건문으로 action.type이 INCREASE가 들어오면, 기존의 state에 action.data를 더해준 것을 return 해주면 된다.

같은 방식으로 -버튼을 만들고, onClickMinus 함수에 액션 객체의 type을 DECREASE, data를 -1을 하고, 외부의 reducer 함수에 조건문을 작성해주면 다음과 같이 -버튼도 구현할 수 있다.

보통 액션 객체의 type이 많아지면 if보다는 switch를 사용하는 게 일반적이다.

 

 

1. useReducer를 통한 refactoring

기존의 상태 관리 함수 onCreate, onUpdate, onDelete를 useReducer를 통해 refactoring 해본다.

useState -> useReducer(state, dispatch)로 바꿔주고, 외부에 매개변수가 state, action인 reducer 함수를 작성해서 진행한다.

 

onCreate

onCreate 함수에 매개변수로 content를 넣어주고, dispatch의 type을 "CREATE", data를 객체형태로 넣어준다.

data의 content는 매개변수로 들어오는 content를 넣어주고 App component밖에서 reducer 함수를 action.type이 "CREATE"일 때 [action.data, ...state]를 설정해주면 추가기능을 구현할 수 있다.

onUpdate

onUpdate 함수에 매개변수로 targetId를 넣어주고, dispatch의 type을 "UPDATE", targetId를 targetId를 넣어준다.

reducer 함수를 action.type이 "UPDATE"일 때, 기존에 작성했던 코드를 적어주면 된다.

onDelete

onDelete 함수에 매개변수로 targetId를 넣어주고, dispatch의 type을 "DELETE", targetId를 targetId를 넣어준다.

reducer 함수를 action.type이 "DELETE"일 때, 기존에 작성했던 코드를 넣어준다.

이처럼 useState를 사용한 코드는 상태 관리 함수들이 길어지는데 반해, useReducer를 사용하면 가독성을 높일 수 있다.


Optimization

Optimization?

Optimization이란 웹 서비스의 성능을 개선하는 모든 행위를 일컫는 말이다.

  • 일반적
    • 서버의 응답속도 개선
    • 이미지, 폰트, 코드 파일 등의 정적 파일 로딩 개선
    • 불필요한 네트워크 요청 줄임
  • React App
    • component 내부의 불필요한 연산, 함수 재생성, 리렌더링 방지

useMemo - 불필요한 연산 방지

"Memoization"이라는 방식을 기반으로 불필요한 연산을 다시 수행하지 않도록 React App을 최적화하도록 도와줌

"Memoization"이란 반복되는 연산을 수행할 때, 계속해서 새로운 연산을 하는 것이 아니라, 저장되어 있던 결과값을 바로 돌려주는 기법을 말한다.

최초에 연산을 해놓은 것을 가지고 남은 연산들에 사용해서 최적화 할 수 있다.

실습

total, done, notDone 개수를 카운트해서, 화면에 렌더링하게 보여주는 코드를 작성한다.

지금 코드는 filter를 사용했으니 item의 개수가 늘어날 수록 순회를 더 많이 하게 되고, 값이 바뀔 때마다 연산이 되고 있다. 데이터를 추가, 수정, 삭제하는 것도 아니고 검색만 하는데 연산을 하는 것 자체가 메모리 낭비일 수 있다. 즉, 새로운 데이터가 추가될 때, 수정될 때, 삭제될 때만 연산하는 것이 더 좋다.

이럴 때 useMemo 훅을 사용한다. useMemo를 사용하면 이러한 연산 자체를 memoization할 수 있다. 그리고 특정 조건이 만족했을 때에만 다시 연산하도록 설정할 수 있다.

useMemo를 import하고, useMemo의 첫번째 매개변수로 콜백함수를 넣고 두번째 매개변수로 배열을 넣으면 된다. 이 배열은 useEffect에서 배웠던 의존성 배열이다. useEffect에서 deps에 들어가는 배열이 바뀔 때 콜백함수를 실행한다라고 배웠다. useMemo도 똑같이 deps에 포함된 값이 변경되었을 때만 콜백함수를 실행한다. 콜백함수가 반환하는 값을 useMemo도 그대로 반환을 한다.

콜백함수에는 memoization하고 싶은 연산을 넣어주면 된다. 즉 우리는 count하는 부분을 모두 콜백함수에 넣어주면 된다. 넣어준 콜백함수는 totalCount, doneCount, notDoneCount를 반환해주고 있으므로 useMemo도 똑같이 반환을 해준다. todos의 값이 변경될 때마다 연산을 실행된다.

위와 같이 useMemo를 이용해서 코드를 작성하면, 검색하는 등의 todo 데이터가 변경되지 않더라도 연산을 수행하던 방식이, 값이 변경될 때만 연산을 수행하는 코드로 바뀌었다.


React.memo - 불필요한 리렌더링 방지하기

memo?

React.memo는 component를 인수로 받아 최적화된 component로 만들어 반환한다. 최적화된 component는 props를 기준으로 memoization된다.

memoization된 component는 부모가 리렌더링되더라도 자기의 props 값이 바뀌지 않으면 리렌더링되지 않게 최적화가 된다.

실습

위 사진을 보면 체크박스를 클릭할 때마다 리렌더링되는 것들을 하이라이트해서 보여주는 것이다. 자세히 보면 Header 부분은 props로 전달되는 값도 없는데 계속해서 리렌더링되고 있다.

Header component에서 memo를 import해주고, export default memo(Header)를 해준다. 그러면 자신의 props의 값이 변하지 않을 경우에는 리렌더링 하지 않게 된다.


useCallback - 불필요한 함수 재생성 방지하기

// 깊은 비교를 위해, memo(컴포넌트, 값이 다르면 memo 안하는 콜백함수) ==> useCallback 으로 함수 대체
// memo(TodoItem)만으로 리렌더링 되는 이유는? App 이 리렌더링되면서, 함수(객체)의 주소값이 계속 바뀐다.
// props 값이 바꼈을 때만 리렌더하고, 그 외(함수나 값이 안바뀌면) true. 메모하고, 리렌더하지마.

export default memo(TodoItem, (prevProps, nextProps)=>{
    if (prevProps.id !== nextProps.id) return false; // 메모하지마.-> 리렌더
    if (prevProps.content !== nextProps.content) return false; // 리렌더
    if (prevProps.isDone !== nextProps.isDone) return false;
    if (prevProps.createDate !== nextProps.createDate) return false;    
    return true; // 리렌더 하지마.
})

memo는 얕은 비교를 하기 때문에, 객체의 속성 값을 직접 비교해서 다른 경우만 리렌더링하고,  return true (메모하고, 리렌더링 하지마.)

위의 코드는 TodoItem들에서 props값이 변경된 item만 리렌더링해주는 코드인데, memo를 사용하면 props 값이 바뀌었는지 확인하는 콜백함수를 매개변수로 넣어줘야 한다. 이렇게 매번 콜백함수를 작성해서 넣어주면 번거로워질 것이다.

이럴 때 useCallback을 사용한다. import useCallback을 해주고, return문 위에서 useCallback을 사용해주면 된다. 첫번째 매개변수로는 최적화하고 싶은 함수, 즉 불필요하게 재생성되지 않도록 방지하고 싶은 함수를 넣어준다. 두번째 매개변수로는 deps를 넣어주면 된다.

이제 useCallback은 첫번째 매개변수로 전달한 콜백함수를 그대로 생성해서 반환해준다. 이 함수를 deps가 변경될 때에만 다시 생성하도록 최적화해준다. 즉 함수를 memoization해주는 것이다. deps로 빈 배열을 넣어주면, mount될 때만 함수가 생성되고, 리렌더링이 아무리 많이 되더라도 나머지 경우에는 생성하지 않게 된다.

기존의 onCreate, onUpdate, onDelete함수들을 useCallback을 통해서 리렌더링될 때 계속해서 생성되지 않게 최적화를 해준다.

이렇게 되면 작성했던 TodoItem component의 memo 메서드 안의 두번째 인자를 모두 지워줘도 onCreate, onUpdate, onDelete함수가 재생성되지 않는다.


Optimization을 언제, 어떤 것들?

최적화는 맨 마지막에 하는 것이 좋다. 항상 기능을 구현하는 것이 먼저가 되어야 하고, 기능이 완성이 되면 그 뒤에 최적화를 하는 것이 일반적인 방법이다.

모든 것들에 최적화를 하면 안되고, 꼭 필요할 것 같은 연산, 함수, component들에만 최적화를 적용하는 것이 좋다. 단순한 화면을 보여주는 component는 최적화를 진행하는 것보다 단순히 리렌더링하는 것이 더 빠를 수 있다. 사소한 component까지 모두 최적화를 진행하지는 않는다. 보통 무거운 연산이나 반복되는 연산이 있는 것들을 진행한다.

Context

React Context는 component간의 데이터를 전달하는 또 다른 방법이다. 기존의 Props가 가지고 있던 단점을 해결할 수 있다.

Props Drilling

Props는 부모에서 자식으로만 값을 전달할 수 있었다.

만약 위와 같이 계층구조가 두 단계라면, App에서 ChildB로 데이터를 직접 전달할 수 없다. 어쩔 수 없이 ChildA가 중간 다리 역할을 한다. App -> ChildA -> ChildB로 데이터가 전달된다.

기존에 작성했던 todo-list 앱도 Props를 사용했기 때문에 App -> List -> TodoItem으로 데이터를 전달하는 방식으로 구현했다. 이러하게 직접 데이터를 전달하지 않는 구조는 당연히 좋지 않다.

지금은 중간 다리 역할을 하는 component는 1개라서 괜찮은데, 구조와 로직이 복잡해지면 App -> ......... -> 이러하게 여러 component를 거치는 구조가 될 수 있다. 만약 props로 전달하는 값이 바뀐다면 모든 props를 수정해야하는 일이 발생할 수도 있다.

이런 구조를 Props Drilling이라고 한다.

 

Context?

Context를 사용하면 이러한 Props Drilling을 해결할 수 있다. Context는 데이터 보관소같은 객체이다. Context를 생성한 다음에 필요한 데이터들을 넣어놓으면, 원하는 component가 직접 가져다가 사용할 수 있다.

Context를 여러개 만들어서 각각의 component가 사용할 수 있게 할 수도 있다.

 

실습

현재 코드를 보면 App -> List -> TodoItem의 방향으로 Props가 전달되고 있다. 이 상태를 Props Drilling이 발생했다고 볼 수 있다.

Context를 사용해서 Props Drilling을 없애주기 위해 App component에서 createContext를 import하고, 새로운 Context를 생성해준다. Context를 생성할 때 component 밖에서 생성을 해준다. 왜냐하면 Context는 단순히 데이터를 저장해서 사용하게끔 해주면 되는데, component안에서 만들어주면 계속해서 리렌더링되면서 함수를 실행하기 때문이다. 따라서 계속해서 다시 생성해줄 필요가 없기 떄문에 밖에 만들어준다.

Context안에 Provider라는 속성이 있는데, 공급할 데이터를 설정하거나, 데이터를 공급받을 component들을 설정하기 위해서 사용한다.

App component에서 TodoContext.Provider안에 데이터를 담아주면 Editor, List, TodoItem component들이 사용할 수 있게 된다.

TodoContext.Provider로 3개의 component들을 감싸주고, value안에 전달할 데이터들을 넣어주면 아래 component들이 직접 사용할 수 있게 된다. 기존의 component들에 전달하던 Props를 모두 삭제해주고, 이제 context로부터 데이터를 받아온다.

Editor component에서 import TodoContext, useContext를 해주고, 기존에 받아오던 onCreate Props를 지워준다. {onCreate} = useContext(TodoContext)를 사용해서 받아오면 된다. 나머지 List, TodoItem component들도 원하는 데이터를 useContext로 구조분해할당해서 가져오면 된다.

 

Context 분리하기

useContext를 사용해서 원하는 데이터를 직접 가져와 사용하게 구현을 했다. 기능적으로는 문제가 없는데, 다시 모든 component들이 리렌더링되고 있다.

이 문제가 발생한 이유는 Provider component도 React의 component이기 때문에 App component로부터 제공받는 todos, onCreate, onUpdate, onDelete를 감싸고 있는 객체가 바뀌면 리렌더링이 발생한다.

그런데 우리는 받는 props가 바뀌지 않으면 리렌더링을 발생시키지않도록 memo를 사용했었다. 그러나 리렌더링되는 이유는 새로운 Todo를 생성, 수정, 삭제할 때 App component의 todos가 변경되어 App component가 리렌더링이 되는데, 그때 Provider component에게 전달하는 객체 자체가 다시 생성되기 때문이다.

이 문제는 state로써 변경될 수 있는 것들은 TodoStateContext, 변경되지 않는 값들은 TodoDispatchContext로 분리하면 해결할 수 있다.

App component에서 todos는 TodoStateContext에 저장해서 List에서 사용하고, onCreate, onUpdate, onDelete는 TodoDispatchContext에 저장해서 Editor, TodoItem에서 사용하게 하면 된다. 이렇게 되면 todo가 생성, 수정, 삭제된다면 todos를 가지고 있는 List만 리렌더링되고 나머지 component들은 todos를 가지고 있지 않기 떄문에 리렌더링되지 않을 것이다.

App component에서 TodoStateComponent, TodoDispatchContext 2개를 생성해준다.

2개를 Provider로 todos와 onCreate, onUpdate, onDelete를 value로 갖게 한다. 이때 onCreate, onUpdate, onDelete는 다시 불리지 않도록 useMemo를 지난 번처럼 사용해준다. 그리고 나서는 각각의 component에 필요한 데이터를 가져와서 사용하면 된다.

결과를 보면 변경이된 TodoItem만 리렌더링되는 것을 확인할 수 있다.

 

'리액트' 카테고리의 다른 글

코딩앙마 VOCA  (0) 2024.06.17
리액트/JS 뮤직 앱  (0) 2023.01.05
코딩애플 리액트 part1 (11/9~)  (0) 2022.11.11

댓글