상태 관리

- 상태 관리 기술이란 앱 상에서의 데이터를 메모리 등에 저장하고 하나 이상의 컴포넌트에서 데이터를 공유하는 것을 의미한다.

  • 컴포넌트 안에서의 상태, 여러 컴포넌트 간의 상태, 전체 앱의 상태 관리를 모두 포함한다.

MPA와 SPA에서의 상태 관리

  • MPA에서는 서버의 데이터를 이용해 페이즈를 렌더링하므로, 클라이언트의 데이터와 서버의 데이터가 큰 차이를 가지지 않는다.
  • SPA에서는 자체적으로 데이터를 가지고, 서버와의 동기화가 필요한 데이터만을 처리한다. 그 외 데이터는 Client만으로 데이터 유지.

상태 관리 기술의 도입

  • 상태가 많지 않거나, 유저와의 인터렉션이 많지 않다면 매 작업 시 서버와 동기화하더라도 충분하지만
  • 앱이 사용하는 데이터가 점점 많아지고, 유저와의 인터렉션 시 임시로 저장하는 데이터가 많아지는 경우 상태관리를 고려.
  • 프론트엔드 뿐만 아니라, 백엔드와의 데이터 통신도 충분히 고려해야 한다.
    ex) GraphQL

상태 관리 기술의 장점:

  • 높은 품질의 코드를 작성하는 데 유리하다.
  • 성능 최적화, 네트워크 최적화 등에 유리하다.
  • 데이터 관리의 고도화가 가능하다.
    ex) localStorage를 활용한 persist state

상태 관리 기술의 단점

  • Boilerplate 문제.
  • 파악해야 할 로직과 레이어가 많아짐.
  • 잘못 사용할 경우, 앱의 복잡도 만을 높이거나, 성능을 악화.
    ex) global state의 잘못된 활용시 앱 전체 리렌더링 발생.

상태 관리 기술이 해결하는 문제

데이터 캐싱과 재활용

  • SPA에서 페이지 로딩 시마다 모든 데이터를 로딩한다면, 사용자 경험 측면에서 MPA를 크게 넘어서기 힘들어진다.
  • 오히려 네트워크 요청 수가 많아져 더 느릴 수도 있기 때문이다.
  • 변경이 잦는 데이터가 아니라면, 데이터를 캐싱하고 재활용해야 한다.
  • 변경이 잦다면, 데이터의 변경 시점을 파악해 최적화해야 한다.
    ex) 일정 시간마다 서버에 저장, 타이핑 5초 후 서버에 저장. (변경이 잦다)

Prop Drilling

  • 컴포넌트가 복잡해지는 경우, 상위 부모와 자식 컴포넌트 간의 깊이가 커짐.
  • 최하단의 자식 컴포넌트가 데이터를 쓰기위해 최상위 컴포넌트부터 데이터를 보내야 하는 상활이 발생한다. (즉 중간에 데이터를 활용하지 않아도 전달하기 위해 prop를 받는 경우가 생긴다.)
  • Context API 등을 활용해서, 필요한 컴포넌트에서 데이터를 가져올 수 있다.
  • Context API를 활용하면 컴포넌트 간의 결합성을 낮출 수 있다.

Flux Pattern

  • 2014년 Facebook에서 제안한 웹 애플리케이션 아키텍처 패턴.
  • 일방향 데이터 흐름을 활용하여 데이터 업데이트와 UI반영을 단순화 하였다.
  • React의 UI 패턴인 합성 컴포넌트와 어울리도록 설계됨
  • redux, react-redux 라이브러리의 선행 패턴이다.

unidirectional data flow

Store(Data Source)를 업데이트하면 View가 업데이트 되는 등 한 방향으로 데이터가 흐르는 개념이다.

Flux 패턴과 MVC 패턴 비교

MVC

  • MVC 패턴에서는 View의 특정 데이터를 업데이트하면 연쇄적인 업데이트가 일어나 앱이 커질수록 업데이트의 흐름을 파악하기 힘들다.
  • View에서 특정 데이터를 업데이트하면 연쇄적인 업데이트가 일어난다.
  • 특정 유저의 인터렉션이 여러 UI 컴포넌트가 사용하는 데이터에 영향을 줄 때, 앱의 복잡도와 업데이트 흐름을 따라가기 힘들다.

MVC 패턴의 Model은 데이터 소스로 볼 수 있고, View는 사용자가 보는 화면단이라고 볼 수 있다. 하나의 Model에서 다수의 View들이 데이터를 불러오는 형태이다. 여기에서 MVC 패턴은 Bidirectional(양방향) 데이터 흐름을 갖는 구조를 하고 있다. 즉, 서로 연결된 상태인 경우 View가 업데이트되면 Model 또한 업데이트 된다.

Flux

  • 반면 Flux는 하나의 Action이 하나의 Update만을 만들도록 한다.
  • 하나의 유저 인터렉션 당 하나의 Update만을 만들도록 한다.
  • Data와 업데이트가 한 방향으로 흐르므로 UI의 업데이트를 예측하기 쉽다.

Store(데이터 소스) -> 다수의 View. View가 업데이트 되어도 Store는 업데이트 되지 않는 unidirectional data flow

Flux의 구조

  • Action -> Dispatcher -> Store -> View 순으로 데이터가 흐른다.

    Action: View가 Store를 업데이트시키기 위해 생성한 것. Reducer에 넘겨진 후, Store를 업데이트한다.

  1. Store는 미리 Dispatcher에 callback을 등록해, 자신이 처리할 Action을 정의한다.

    callback: 이벤트에 따라 실행될 함수

  2. Action Creator는 Action을 생성하여 Dispatcher로 보낸다.
  3. Dispatcher는 Action을 Store를 넘긴다.
  4. Store는 Action에 따라 데이터를 업데이트하고, 관련 View로 변경 이벤트를 발생시킨다.
  5. View는 그에 따라 데이터를 다시 ㅂ받아와 새로운 UI를 만든다.
  6. 유저 인터렉션이 발생하면 View는 Action을 발생시킨다.

React Hooks을 통한 상태 관리

상태 관리에 사용되는 Hooks

  • 외부 라이브러리 없이 React가 제공하는 Hook 만으로 상태 관리를 구현하기 위해 사용된다.
  • 함수형 컴포넌트에 상태를 두고, 여러 컴포넌트 간 데이터와 데이터 변경 함수를 공유하는 방식으로 상태를 관리.

useState

  • 단순한 하나의 상태를 관리하기에 적합
  • const [state, seState] = useState(initState | initFn)
  • state가 바뀌면, state를 사용하는 컴포넌트를 리렌더한다.
  • useEffect와 함께, state에 반응하는 Hook을 구축한다.

useRef

  • 상태값이 변경되면, 리렌더링이 일어나기 때문에 이와 상관없이, 상태가 바뀌어도 리렌더링하지 않는 상태를 정의할 때 사용한다.
  • 즉, 상태가 UI의 변경과 관계없을 때 사용.

    setTimout의 timerId를 저장하는 것 등이 해당

  • uncontrolled component의 상태를 조작하는 등 리렌더링을 최소화하는 상태 관리에 사용된다.

    Dynamic Form의 예시를 들 수 있다.

useContext

  • 컴포넌트와 컴포넌트 간 상태를 공유할 때 사용.
  • 부분적인 컴포넌트들의 상태 관리, 전체 앱의 상태 관리를 모두 구현한다.
  • Context Provider 안에서 렌더링 되는 컴포넌트는, useContext를 이용해 깊이 nested 된 컴포넌트라도 바로 context value를 가져올 수 있다.
  • context vlaue가 바뀌면 내부 컴포넌트는 모두 리렌더링된다.

useReducer

  • useState보다 복잡한 상태를 다룰 때 사용한다.
  • 별도의 라이브러리 없이 flux pattern에 기반한 상태 관리를 구현한다.
  • const [state, dispatch] = useReducer(reducer, initState)
  • nested state 등 복잡한 여러 개의 상태를 한꺼번에 관리하거나, 어떤 상태에 여러 가지 처리를 적용할 떄 유용하다.
  • 상태 복잡하다면, useState에 관한 callback을 내려주는 것보다 dispatch를 prop으로 내려 리렌더링을 최적화하는 것이 권장된다.

useState를 활용한 상태 관리

  • 상위 컴포넌트에서 state와 변경 함수를 정의한다. 그 state변경 함수를 사용하는 컴포넌트까지 prop으로 내려주는 패턴이다.
  • state가 변경되면, 중간에 state를 넘기기만 하는 컴포넌트들도 모두 리렌더링된다.
  • 상태와 상태에 대한 변화가 단순하거나, 상대적으로 소규모 앱에서 사용하기에 적합하다.

TodoApp

function TodoApp() {
	const [todos, setTodos] = useState([]);
  	const [filter, setFilter] = useState('all');
  	const [globalId, setGlobalId] = useState(3000);
  
  	const toggleTodo = (id) => {
    		setTodos((todos)=>{
            	  todos.map((todo)=>{
                  	todo.id === id ? {...todo, completed: !todo.completed}
                  }: todo)
            })
    	}
     	const deleteTodo = (id) => {
        	setTodos((todos)=> todos.filter((todo)=> todo.id !== id));
        };
      	const addTodo = (title => {
        	setTodos((todos)=> [{title, id: globalId + 1}, ... todos]);
          	setGlobalId((id)=> id+1);
          
         return <TodosPage
          	state ={{todos, filter}}
            	toggleTodo={toggleTodo}
    		addTodo={addTodo}
  		deleleTodo={deleteTodo}
  		changeFilter={setFilter}
  		/>
        })
}

TodosPage

function TodosPage({state, addTodo, deleteTodo, toggleTodo, changeFilter}){
	const filterTodos = state.todos.filter((todo)=>{
    		const {filter} = state;
      		return (
            		filter === 'all' ||
              		(filter === 'completed' && todo.completed) ||
              		(filter === 'todo' && !todo.completed)
            );
    })
    
    return (
    	<div>
      		<h3>TodosPage</h3>
      		<TodoForm onSubmit={addTodo}/>
		<TodoFilter filter={state.filter} 
		changeFilter={changeFilter}/>
            	<ToDoList
		 todos={filteredTodos}
		 toggleTodo={toggleTodo}
		 deleteTodo={deleteTodo}
		 />
      	</div>
    );
}

TodoForm

function TodoForm({onSubmit}) {
  const [title, setTitle] = useState('');
  return (
    <form
      onSubmit={(e) =>{
    	e.preventDefault();
        onSubmit(title);
        setTitle('');
      }}
     >
      <label htmlFor ='todo-title'>Title</label>
      <input id='todo-title' type='text' 
	name='todo-title' onchange={(e)=>
    	setTitle(e.target.value)} value={title} />
      <button type='submit'>Make</button>
      </form>
    	);
}

todoList

function TodoList({todos, toggleTodo, deleteTodo}) {
  return (
  	<ul>
    	  {todos.map(({title, completed, id}) => (
  	    <li onClick{()=> toggleTodo(id)}>
    	    <h5>{title}</h5>
	    	<div>
    		  {completed ? 체크아이콘: 쓰기 아이콘}
       		  <button onClick={()=> 
		  deleteTodo(id)}>Delete</button>
 	    	</div>
	    </li>
  	))} 
    	</ul>
  );
}

TodoFilter

function TodoFilter({filter, changeFilter}) {
  return (
    <div>
    	<label htmlFor='filter'>Filter</label>
    	<select
    	  onChange{(e)=> changeFilter(e.target.value)}
	  id='filter'
	  name='filter'
	 >
          {filterList.map((filterText)=> (
            <option selected={filter === filterText} value={filterText}>
            </option>
          ))};
         </select>
    </div>
  );
}

useContext를 활용한 상태 관리

  • context API를 활용. Provider 단에서 상태를 정의하고, 직접 상태와 변경 함수를 사용하는 컴포넌트에서 useContext를 이용해 바로 상태를 가져와 사용하는 패턴
  • useReducer와 함께, 복잡한 상태와 상태에 대한 변경 로직을
    두 개 이상의 컴포넌트에서 활용하도록 구현이 가능하다.
  • state는 필요한 곳에서만 사용하므로, 불필요한 컴포넌트 리렌더링을 방지하며, Prop Drilling(Plumbing)을 방지하여 컴포넌트 간 결합도를 낮춘다.

TodoContext

const TodoContext = createContext(null);

const initialState ={
	todos: [],
  	filter: 'all',
  	globalId: 3000,
};

function useTodoContext(){
  const context = useContext(TodoContext);
  if (!context){
    throw new Error('Use TodoContext inside Provider.');
  }
  return context;
}

function TodoContextProvider({children}){
  const values = useTodoState();
  return <TodoContext.Provider
  value={values}>{children}</TodoContext.Provider>;
}

function reducer(state, action) {
  switch(action.type) {
    case 'change.filter':
      return {...state, filter:
             action.payload.filter};
    case 'init.todos':
      return {...state, todos:
             action.payload.todos};
    case 'add.todo': {
      return {...state, todos:
             [{title: action.payload.title,
              id: state.globalId + 1}],
             globalId: state.globalId + 1};
    		}
    case 'delete.todo': {
      return {...state, todo:
             state.todos.filter((todo)=> todo.id !==
                              action.payload.id)};
    	}
    case 'toggle.todo': {
      return {...state, todos: state.todos.map((t)
               => t.id === action.payload.id ?
               {...t, completed: !t.compeleted}: t)};
    	}
      deafault: return state;
    }
}

function  useTodoState() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const toggleTodo = useCallback((id)=> dispatch({type: 'toggle.todo', payload: {id} }, []);
  const deleteTodo = useCallback((id)=> dispatch({type: 'delete.todo', payload: {id} }, []);
  const addTodo = useCallback((title)=> dispatch({type: 'add.todo', payload: {title} }, []);
  const changeFilter = useCallback((filter)=> dispatch({type: 'change.filter', payload: {title} }, []);
  const initializeTodos = useCallback((todos)=> dispatch({type: 'init.todos', payload: {todos} }, []);
 
  return {state, toggleTodo, deleteTodo, addTodo, changeFilter, initializeTodos};
}

todoApp

function TodoApp() {
  return (
  	<TodoContextProvider>
    	 <TodosPage/>
        </TodoContextProvider>
  );
}

TodosPage

function TodosPage() {
  const {initializeTodos} = useTodoContext();
  
  // 처음 마운트 되었을 때, todo를 받아와 업데이트한다.
  useEffect(()=>{
    console.log('useEffect');
    fetchTodos().then(initializeTodos);
  }, [initializeTodos]);
  
  return (
  	<div>
    	  <TodoForm/>
    	  <TodoFilter/>
    	  <TodoList/>
        </div>
  );
}

TodoForm

function TodoForm(){
  const {addTodo} = useContext();
  const [title, setTitle] = useState('');
  
  return (
    <form
      onSubmit={(e)=>{
       e.preventDefault();
       addTodo(title);
       setTitle('');
      }}
    >
      <label htmlFor='todo-title>Title</label>
      <input 
	id='todo-title'
	type='text'
	name='todo-title'
	onChange={(e)=> setTitle(e.target.value)}
    	value={title}/>
      <button type='submit'>Make</button>
    </form>
  );
}

TodoList

function TodoList() {
  const {state, toggleTodo, deleleTodo} = useTodoContext();
  const { todos, filter } = state;
  
  const filteredTodos = todos.filter((todo)=>{
    return (
      filter === 'all' ||
      (filter === 'completed' && todo.completed) ||
      (filter === 'todo' && !todo.completed)
    );
  })
  
  return (
    <ul>
     {filteredTodos.map(({title, completed, id}) => (
       <li key={id} onClick={()=> toggleTodo(id)}>
         <h5>{title}</h5>
	 <div>
       	  {completed ? 체크박스아이콘 :펜 아이콘}
	   <button onClick={()=> deleteTodo(id)}>Delete</button>
    	 </div>
     ))}
     </ul>
  )
}
profile
사용자의 편의를 더 생각하고 편안한 UI/UX 개발을 꿈꾸는 프론트엔드 개발자 지망생입니다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN