코드가 복잡해지는 이유
이번 강의에서는 우리의 코드가 복잡해지는 이유는 액션과 계산의 분리가 되지 않았음에 초점이 맞춰있다.
계산과 액션을 구분하라
식(Expression): 값을 생성하며 이를 반환함. 또한 다른 식의 일부가 될 수 있음.
문(Statement): 프로그램의 행동을 지시하며 값을 반환하지 않음. 의미론적으로는 코드의 흐름을 제어하거나 변수를 선언하는 등의 역할을 수행함.
1. 순수 함수 / 부수 효과
순수 효과란 입력에 대해 항상 동일한 값을 반환하며, 부수 효과가 없는 함수이다.
const add = (a, b) => a + b
부수 효과란 함수의 핵심 목적에서 벗어나 외부 세계에 영향을 주는 행위가 포함된 함수이다.
const asyncCall = async () => {
return fetch(/** 비동기 호출 */)
}
위의 코드는 fetch를 통해 서버로 비동기 호출을 하는데, 서버가 다운되거나 요청이 몰려 timeout되는 경우 값 대신 Promise.reject()를 반환한다.
이는 외부에 영향을 주며, 같은 값을 반환받을 수 없는 경우이다.
이것이 부수효과를 줄이고 순수 함수로 React를 구성해야하는 이유다.
2. 데이터 / 계산 / 액션의 구분
- 데이터 : 이벤트에 대한 사실. 문자열, 객체 등 단순한 값 그 자체.
ex) 입력한 정보, API로 읽은 데이터 - 계산 : 입력으로 얻은 출력. 순수 함수, 수학 함수 라고 부르기도 한다.
ex) filter값 찾기, 유효성 확인 - 액션 : 외부 세계와 소통하므로 실행 시점과 횟수에 의존 및 부수 효과를 일으킨다.
ex) 이메일 보내기, DB 읽기
액션을 데이터와 계산으로 부터 가급적 분리하고, 될 수 있다면 계산을 사용하며 액션으로부터 계산을 추출해야한다.
액션을 데이터와 계산으로 부터 분리하는 이유
1) 계산은 외부에 영향을 주지 않기에 테스트 하기 쉽다.
2) 여러번 테스트 해도 문제 발생이 없다.
하지만 액션은 실행 시점과 횟수에 따라 결과가 다르게 나올 수 있으며 외부 세계에 영향을 주기 때문에 재현하기 위해서 많은 노력이 필요하다.
3. 직접 계산 / 액션 구분해보기
import React, { useState } from 'react';
const TodoList = () => {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const [filter, setFilter] = useState('all'); // all, completed, incomplete
const [searchQuery, setSearchQuery] = useState('');
const addTodo = () => {
const newTodo = { text: input, completed: false };
setTodos([...todos, newTodo]);
setInput('');
};
const toggleComplete = (index) => {
const newTodos = [...todos];
newTodos[index].completed = !newTodos[index].completed;
setTodos(newTodos);
};
const filterTodos = () => {
if (filter === 'completed') {
return todos.filter((todo) => todo.completed);
}
if (filter === 'incomplete') {
return todos.filter((todo) => !todo.completed);
}
return todos;
};
const searchTodos = () => {
return filterTodos().filter((todo) => todo.text.includes(searchQuery));
};
return (
<>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<button onClick={() => setFilter('all')}>Show All</button>
<button onClick={() => setFilter('completed')}>Show Completed</button>
<button onClick={() => setFilter('incomplete')}>Show Incomplete</button>
<input
placeholder="Search..."
onChange={(e) => setSearchQuery(e.target.value)}
/>
<ul>
{searchTodos().map((todo, index) => (
<li key={index}>
{todo.text}{' '}
<button onClick={() => toggleComplete(index)}>
{todo.completed ? 'Undo' : 'Complete'}
</button>
</li>
))}
</ul>
</>
);
};
export default TodoList;
- 계산(순수 함수)
- filterTodos: 외부 상태를 변경하지 않으며, 동일한 상태에 대해 항상 동일한 결과 반환
- searchTodos: 외부 상태를 변경하지 않고, 동일한 입력에 대해 동일한 출력
- 액션(부수 효과를 가진 함수)
- addTodo: 외부상태인 todos를 변경하는 부수 효과
- toggleComplete: todos 배열의 상태를 변경하는 부수 효과
- setInput, setFilter, setSearchQuery: React의 useState 훅을 통해 컴포넌트의 상태를 변경하는 함수
액션이 속한 함수는 특징이 있다.
- 부수효과를 일으킨다.
- 컴포넌트 바깥으로 분리가 힘들다.
- 만약 다른 '계산' 로직과 합쳐지면 그들 또한 액션으로 만들어 버린다.
setState가 부수 효과를 일으키는 함수인 이유(암묵적 인자에 대한 의존성)
암묵적 인자란 파라미터를 통해 명시적으로 받지 않고 함수의 동작에 영향을 미치는 외부 변수나 상태
이는 함수의 예측 가능성과 재사용성을 감소시킬 수 있다.
이를 해결 하려면 가능한 한 명시적인 매개변수를 사용하는 것이 좋다.
결론
이처럼 순수 함수는 테스트하기 쉽고 예측 가능한 반면,
부수 효과를 가진 함수는 상태 변경이나 외부 시스템과의 상호작용을 관리해야 하므로 더 주의 깊게 다루어야 한다.
멘토님의 예시코드에서 놀라움을 느꼈다.
메서드 체이닝 스타일을 통해 헬퍼 함수를 만들고, 메서드에 인자를 전달하며 암묵적 인자 사용을 없애고, 각 메서드를 입력된 인자에 대해 매번 동일한 값을 반환하게 설계해 순수 함수의 상태를 유지, 액션 동작을 하는 setTodos 사용을 최소화 했다.
이 날 배운 모드게 들어간 코드인거같다....
'회고록 > React 프리온보딩' 카테고리의 다른 글
React 3회차: SOLID한 컴포넌트 만들기 (0) | 2023.12.14 |
---|---|
React 2회차: 비즈니스 로직 (1) | 2023.12.12 |