본문 바로가기

JS/React Project

📝 메모앱 업그레이드

현재 App 컴포넌트에는 onCreate, onDelete, onEdit  3가지의 상태변화 함수가 존재한다.

이 함수들은 App 컴포넌트 내에만 존재했어야 하는데, 이유는 기존의 상태를 참조했어야 하기 때문이다. 

function App() {

    const [data, setData] = useState([]); 

    const onCreate = () => {
        // 중략
        setData((data)=>[newItem, ...data]);
    }
    
    const onDelete = () => {
    	// 중략
    	setData((data)=> data.filter((it)=>it.id !== targetId));
    }
    
    const onEdit = () => {
    	// 중략
        setData((data) =>
      	  data.map((it) => it.id === targetId ? {...it, content:newContent} : it)
        );
    }
}

위와 같이 setData안의 (data)는 상단의 data를 참조해서 썼어야 했기 때문이며, 컴포넌트 내에서 상태변화 로직이 길고 복잡해지는 것은 좋은 코드가 아니다. 이를 해결하기 위해 컴포넌트 바깥으로 분리를 해주는 것이 좋다. 

 

useReducer를 사용하여 컴포넌트에서 상태 변화 로직을 분리하여 보자. 

 

1. useReducer 

Hook 중 하나로 상태 관리를 위해 사용되며, 일반적으로 useState와 비슷한 목적으로 사용하지만 복잡한 로직을 더 잘 다룰 수 있다. 

 

다음과 같은 형태로 사용된다. 

const [state, dispatch] = useReducer(reducer, 초기값);
  • state : 현재 상태 값으로 다루고 있는 state의 이름 
  • dispatch :  액션을 발생시켜 상태를 변화시키는 함수
  • reducer : 상태변화가 일어나면 처리될 로직을 정의한 함수. 현재 상태와 액션 객체를 받아 새로운 상태를 반환한다. 
  • 초기값 : 컴포넌트가 처음 렌더링될 때 사용될 state의 초기값

useReducer를 사용하면 상태 업데이트 로직을 컴포넌트 외부로 분리하여 정의하고 관리할 수 있기 때문에, 컴포넌트 내에서 상태 업데이트 로직이 길어지는 것을 방지할 수 있으며 이를 통해 코드를 더 구조적이 관리하기 쉽게 만들 수 있다.

 

 

App.js

const reducer = (state, action) => {
                // state : 상태변화 직전의 state
                // action: 어떤 상태변화를 일으켜야 하는지에 대한 정보가 담긴 객체 

    // action객체의 type 프로퍼티와 switch case를 사용하여 관리한다. 
    switch(action.type) {
    
    // 1. onCreate (data: subject, title, content, id)
    case 'CREATE' : {
    
    	// 작성일 생성
        const writtenDate = new Date().getTime();
        
        // newItem 생성 
        const newItem = {...action.data, writtenDate}
    
    	// Create를 통해 생성할 data의 값
    	return [newItem, ...state]
    }
    
    // 2. onDelete (targetId) 
    case 'DELETE' : {
    	// data의 id가 targetId와 같지 않은 것들로만 배열을 재생성하여 return 
    	return state.filter((it) => it.id !== action.targetId);
    }    
    
    // 3. onEdit (targetId, newContent)
    case 'EDIT' : {
    	// data의 id가 targetid와 같다면 해당 요소의 content를 newContent로 설정하고 아니면 원래 요소를 return
    	return state.map((it) => it.id === action.targetId ?
                                 {...it, content : action.newContent} : it);
    }
    
    // 4. default의 경우 값의 변화가 없도록 하기 위해 비워둠. 
    default : 
    
    // return하는 값이 data의 값이 됨.
    return state; 
}

function App() { 

    // 기존의 상태를 관리하던 useState는 지우고 useReducer를 사용한다. 
    // const [data, setData] = useState([]); 
 		
    const [data, dispatch] = useReducer(reducer,[]);


    // 1. onCreate 
    const onCreate = useCallback(

        (subject, title, content) => {

        // 기존의 작성일 함수, newItem, setData는 reducer의 action에서 지정하도록 함.
        dispatch({type:'CREATE', data:{subject, title, content, id:dataId.current }})

        dataId.current += 1;

        // const writtenDate = new Date().getTime()
        
        // const newItem = {
        //   subject, 
        //   title, 
        //   content,
        //   writtenDate,
        //   id: dataId.current,
        // }

        // setData((data)=>[newItem, ...data]);

    },[]);


    // 2. onDelete
    const onDelete = useCallback(
        (targetId) => {

        // 기존 setData를 reducer의 action에서 지정하도록 함.
        dispatch({type:'DELETE', targetId})
          
       // setData((data)=> data.filter((it)=>it.id !== targetId));

    },[]);


    // 3. onEdit 
    const onEdit = useCallback(
    	(targetId, newContent) => {

        // 기존 setData를 reducer의 action에서 지정하도록 함.
        dispatch({type:'EDIT', targetId, newContent})

        // setData( (data) =>data.map((it) => it.id === targetId ? {...it, content:newContent} : it));

    },[]);
}

 

 

2. Context 

현재 App.js에서 만든 onEdit, onDelete Props은 App.js -> MemoBoard -> MemoItem 을 순서로 전달이 되고 있다. 

이때 MemoBaord에서는 사용되지 않지만 전달을 위해서만 사용이 되고 있는데 이를 Prop Drillingd이라 한다. 이는 MemoBoard처럼 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 과정에서 중간에 위치한 컴포넌트가 해당 데이터를 사용하지는 않지만 전달을 위해 Props를 전달받는 상황을 말한다. 이런 Drilling 자체는 문제가 되지는 않지만, 중간 컴포넌트들이 해당 데이터를 사용하지 않는 경우 코드의 가독성을 떨어뜨리고 관리를 어렵게 만든다.(ex: prop의 이름이 바뀌면 모두 일일이 바꿔줘야 함) 

 

이럴 때 Context를 사용하여 이러한 현상을 줄이고 컴포넌트의 계층 구조가 깊어지더라도 데이터를 간단하게 전달할 수 있다. 

Context는 리택트에서 상태를 전역적으로 공유하고 전달하기 위한 내장 인터페이스다. 다른 컴포넌트들과 데이터를 주고받을 때 사용되며, 컴포넌트들이 Context를 통해 데이터를 읽고 쓸 수 있도록 하여 상태를 전역적으로 공유하고 관리할 수 있게 한다. 

 

 

* Redux와의 차이점 

더보기

Context와 Redux는 모두 리액트 애플리케이션에서 상태 관리를 위한 도구로 사용되며 데이터를 전역적으로 관리하는 데 도움을 준다. 하지만 몇 가지 중요한 차이점이 있다. 

 

1. 범위(Scope)

- Context: 리액트의 내장 기능으로, 컴포넌트 계층 안에서 데이터를 전달하고 공유하는 데 사용된다. 주로 컴포넌트들을 거치지 않고도 데이터를 하위 컴포넌트로 전달할 때 유용하다. 

- Redux: 외부 라이브러리로, 리액트 앱  전체 상태를 하나의 Store에 저장하고 관리한다. 모든 컴포넌트들이 해당 Store에 접근하여 데이터를 사용할 수 있다. 

 

2. 복잡성(Complexity) 

- Context: 상대적으로 간단한 상태 관리를 위해 사용된다. (단일 컴포넌트의 상태를 다른 컴포넌트와 공유, 중간 컴포넌트에서 데이터를 전달할 때 쉽게 사용할 수 있음) 

- Redux: 복잡한 애플리케이션의 상태 관리를 위해 사용된다. (많은 컴포넌트들이 상태를 공유하고 다양한 action과 reducer로 상태를 업데이트하는 경우 유용) 

 

3. 중앙 집중화(Centralization)

- Context : 기본적으로 컴포넌트 계층 안에서만 데이터를 공유하므로 상태가 중앙집중화되지 않는다. 

- Redux: 하나의 Store에 모든 상태를 중앙 집중화하여 관리한다. 이로 인해 앱의 상태 변화를 추적하고 디버깅하기 용이하다. 

 

4. 사용 사례 

- Context: 단순 데이터 공유, 테마 변경, 권한 관리 

- Redux: 복잡한 상태 관리, 비동기 작업, 상태의 불변성 유지와 같은 고급 상태 관리 

 

즉, Context는 리액트에서 내장되어 있으며 컴포넌트 계층 안에서 상태를 공유하기 위해 사용되고, Redux는 외부 라이브러리로 앱 전체의 상태를 중앙 집중화하여 관리하여 사용된다. 더 간단한 상태관리에는 Context를, 더 복잡한 상태관리에는 Redux를 사용할 수 있다. 

 

 

사용 단계 

1. createContext()를 사용하여 컴포넌트 외부에 새로운 Context 객체 생성

export const MemoStateContext = React.createContext();
// Context를 외부로 내보내줘야만 다른 컴포넌트들이 해당 Context에 접근할 수 있기 때문에 export

 

2. Provider를 통하여 공급 : Context의 데이터를 하위 컴포넌트로 전달하기 위해 Provider 컴포넌트를 사용. 

function App() {
	return (
        // value 안에는 전역으로 전달할 값을 넣음(메모앱에선 data state)
    	<MemoStateContext.Provider value={data}>
        
        	{ 이 Context 안에 위치할 자식 컴포넌트들 }
        
        </MemoStateContext.Provider>
    );
}

 

Provider도 컴포넌트이기 때문에 data와 함께 data의 상태를 변화시키는 onCreate, onDelete, onEdit과 같은 함수들도 같이 내려주게 된다면, 해당 컴포넌트에 속한 상태나 함수가 변경될 때 컴포넌트가 리렌더링된다. 이는 Provider에 포함된 값이 바뀔 때마다 하위 컴포넌트들도 함께 리렌더링되어 성능에 문제를 줄 수 있고 지금까지 해 온 최적화가 의미가 없어지게 된다. 

 

이런 경우 중첩된 Provider를 활용하여 해결할 수 있다. 

앞서 만든  MemoStateContext는 data의 state만 공급하기 위해 존재하고, onCreate와 같은 상태를 변화시키는 dispatch 함수는 따로 만들어 자식으로 배치한다. 

 

export const MemoStateContext = React.createContext();
export const MemoDispatchContext = React.createContext();

function App() {
		
        // 상태변화 함수들를 하나로 묶어 prop으로 전달
        // useMemo를 사용하여 App 컴포넌트가 재생성될 때 아래 함수들도 재생성되지 않도록 함
        const memoizedDispatches = useMemo(()=> {
        	return {onCreate, onDelete, onEdit}
     	 },[])
        
        return(
            <MemoStateContext.Provider value={data}>
                <MemoDispatchContext.Provider value={memoizedDispatches}>
                </<MemoDispatchContext.Provider>
            </MemoStateContext.Provider>
        );

}

 

 

3. useContext(Hook) : Context의 데이터를 읽어옴 

 

MemoBoard.js

                  // {onEdit, onDelete, memoBoard} 
                  // prop을 작성하지 않아도 context를 사용하여 받아올 수 있다. 
const MemoBoard = () => {

    // useContext 사용하여 memoBaord(data)를 받아옴
    const memoBoard = useContext(MemoStateContext);

    return <div className="memoBoard">

        <div className="memoBoardArea">
            {memoBoard.map((it)=>(
            	// prop으로 onEdit, onDelete을 전달하지 않아도 된다. 
                <MemoItem key={it.id} {...it} />
            ))}
        </div>
    </div>
}

export default MemoBoard;

 

MemoItem.js

                  // onEdit, onDelete 삭제 
const MemoItem = ({id, subject, title, content, writtenDate}) => {

    // useContext를 통해 onDelete, onEdit 받아와서 사용하면 된다. 
    const { onDelete, onEdit } = useContext(MemoDispatchContext);

	// ... 중략
}

 

 

 

완성!

 

GitHub

https://github.com/davin1221/SimpleMemo

 

GitHub - davin1221/SimpleMemo: 리액트 실습 - 간단한 메모장 만들기

리액트 실습 - 간단한 메모장 만들기 . Contribute to davin1221/SimpleMemo development by creating an account on GitHub.

github.com

 

Demo Site

https://adorable-cucurucho-ae6710.netlify.app/

 

React App

 

adorable-cucurucho-ae6710.netlify.app