react-beautiful-dnd는 3개의 요소로 이루어져있습니다.

 

  1. DragDropContext
    • 드래그를 사용할 영역을 의미합니다
    • contextAPI의 Provider같은 느낌입니다. (감싸주는 느낌..?)
  2. Droppable
    • Drag해서 놓을 Drop할 영역입니다.
    • Draggable을 감싸서 사용합니다.
  3. Draggable
    • Drag 할 Item 입니다. 
    • 우리가 이용할 Item컴포넌트를 Draggable로 감싸주면 됩니다.

 

DragDropContext

DragDropContext는 3개의 prop이 있습니다.

  1. onDragStart
    • Darg를 시작할 때 (Drag 할 Item을 클릭하고 움직이기 시작할 때) 호출
  2. onDragEnd
    • Darg가 끝났을 때 (마우스를 땔때) 호출
  3. onDragUpdate
    • Drag중에 발생하는 변화가 생겼을때 호출

 

Droppable

1. Droppable에는 droppabledId 라는 props가 있습니다.

해당 Id는 Context 안에서 유일해야합니다. (영역을 구분지어야 하기 때문)

 

Droppable에서 사용할 Draggable에 해당하는 자식 컴포넌트는 Element를 받는게 아닌

함수 형태로 받아야 합니다.

 

  <Droppable droppableId={list.id}>
        {(provided: any) => (
          <div>
              {items.map((item, i) => (
                <Item key={item.id} item={item} index={i} />
              ))}
              {provided.placeholder}
          </div>
        )}
      </Droppable>

 

provided에는 droppable에 사용할 props가 모여있습니다.

1. data-rbd-droppable-context-id

2. data-rbd-droppable-id

직접 쓰면.. 

  {(provided: any) => {
          return (
            <div
              data-rbd-droppable-context-id={
                provided.droppableProps["data-rbd-droppable-context-id"]
              }
              data-rbd-droppable-id={provided.droppableProps["data-rbd-droppable-id"]}
              ref={provided.innerRef}
            >

사실 이렇게 쓰고싶은사람은 없으니.

 

  {(provided: any) => {
          return (
            <div {...provided.droppableProps} ref={provided.innerRef} >

스프레드를 씁시다.

 

2. 앗. ref도 지정해줘야합니다. 

 

3. 마지막으로 placeholder 라는 프로퍼티가 있습니다.

 

droppable에 변화가 생긴다면 해당 부분을 처리해주는 역할을 합니다.

 

  <Droppable droppableId={list.id}>
        {(provided: any) => {
          return (
            <div {...provided.droppableProps} ref={provided.innerRef}>
              <>
                {items.map((item, i) => (
                  <Item key={item.id} item={item} index={i} />
                ))}
                {provided.placeholder}
              </>
            </div>
          );
        }}
      </Droppable>

 

 

Draggable

1. Draggable은  draggableId와 index가 필수입니다.

 

Item 컴포넌트를 Drag가 가능하게 하려면 Draggable로 감싸야합니다.

 

Droppable처럼 함수 형태로 작성해야합니다.

 

const Item = ({ item, index }) => {
    <Draggable draggableId={item.id} index={index}>
      {(provided: any) => {
        return (
          <div
            {...provided.draggableProps}
            {...provided.dragHandleProps}
            ref={provided.innerRef}
          >
            {item.content}
          </div>
        );
      }}
    </Draggable>
}

draggableProps, dragHandleProps, 그리고 ref까지 작성 해줘야합니다.

 

draggableProps : Drag 가능한 요소에 적용되는 속성을 나타내며, 해당 요소의 이벤트 리스너 및 기타 속성을 정의할 수 있습니다.

(context에서 작성한 onDragStart , onDragEnd 같은 친구들..)

dragHandleProps : Drag 액션을 처리하는 드래그 핸들에 적용되는 속성을 나타내며, Drag가 발생하는 영역을 정의할 수 있습니다.

 


onDrag 함수들에 대해

{
	draggableId: 'itemList',
    type: 'TYPE'
    reason: 'DROP',
    source: {
    	droppableId: 'item1',
        index:0,
    }, 
    destination: {
    	droppableId: 'item1',
        index:1, 
    }, 
}

draggableId : Drag 한 객체의 id

source : Drag가 시작된 위치  Droppable의 Id와 시작할 때 item의 index

destination : Drag가 끝나는 위치 Droppable의 Id와  끝날때 Item의 index Droppable의 밖에 드래그 할 경우 null

 

 

source와 destination을 이용해야합니다.

  const onDragEnd = (result: DropResult) => {
    const { destination, source, draggableId } = result;
    if (!destination) return;
    if (destination.droppableId === source.droppableId && source.index === destination.index)
      return;

    const newItemIds = Array.from(column.itemIds);

    //시작지점 source의 아이탬의 index를 잘라냅니다 (위치를 바꿔주기위해)
    newItemIds.splice(source.index, 1);
    //아이탬의 도착지 destination의 index 위치에 draggableId를 추가합니다 (잘라낸 아이탬의 id)
    newItemIds.splice(destination.index, 0, draggableId);

    //바뀐 newItemIds를 갱신
    const newColumn = {
      ...column,
      itemIds: newItemIds,
    };

    //갱신한 값을 Data를 가져와서 바꿔줍니다
    const newData = {
      ...data,
      columns: {
        ...data.columns,
        [newColumn.id]: newColumn,
      },
    };

    setData(newData);

}

 

 

다른 Droppable에도 이동하기

 

처음엔 어떻게 하는건지.. 어렵게 생각했었는데..

크게 다르지 않았습니다.

 

기존소스를 이용하면서 바뀐부분은 주석!

 

  const onDragEnd = (result: DropResult) => {
    const { destination, source, draggableId } = result;

    if (!destination) return;
    if (destination.droppableId === source.droppableId && source.index === destination.index)
      return;

    const sourceColumn = data.columns[source.droppableId]; // 출발지의 droppable정보
    const destinationColumn = data.columns[destination.droppableId];// 도착지점의 droppable정보
    
    if(source.droppableId === destination.droppableId){
        //기존소스
      
        /*column 을 sourceColumn 으로 변경
		어짜피 시작과 끝이 같은 영역일 경우에만 동작하므로 
        source나 destination 아무거나 사용해도 괜찮습니다.
        그 영역의 정보를 가지고 오는것이 중요
		*/
      const newItemIds = Array.from(sourceColumn.itemIds);    
      newItemIds.splice(source.index, 1);
      newItemIds.splice(destination.index, 0, draggableId);

      //...column 을 ...sourceColumn 으로 변경
      const newColumn = {
        ...sourceColumn,
        itemIds: newItemIds,
      };


      const newData = {
        ...data,
        columns: {
          ...data.columns,
          [newColumn.id]: newColumn,
        },
      };

      setData(newData);
      //여기까지 기존소스
      
    }else{
      //출발지와 도착지가 이제 다르기 때문에 따로 만들어줍니다.
      
      const sourceItemIds = Array.from(sourceColumn.itemIds);
      const destinationItemIds = Array.from(destinationColumn.itemIds);
      
      sourceItemIds.splice(source.index, 1);
      destinationItemIds.splice(destination.index, 0, draggableId);

      const newSourceColumn = {
          ...sourceColumn,
          itemIds: sourceItemIds,
      };

      const newDestinationColumn = {
          ...destinationItemIds,
          itemIds: destinationItemIds,
      };

      const newData = {
        ...data,
        columns: {
            ...data.columns,
            [newSourceColumn.id]: newSourceColumn,
            [newDestinationColumn.id]: newDestinationColumn,
        },
      };

      setData(newData);
    }
};

리덕스 툴킷!

 

와.. 제가 이거 이해하려고 한참을 공부했내요..

 

제가 이해한게 틀렸다면 언제든지 지적 해주세요! 

 

아래에서 사용할 단어를 간단하게 정리했습니다

 

처음엔 어려워서 이해가 가지 않을 수 있습니다.

 

어렵더라도 끝까지 읽어보시고 마지막에 간단하게 정리된 내용까지 읽어보신다면...

아마도..?

 

store : store 어플리케이션의 전체 상태(state) 저장하고, 이를 변화시키는 action reducer 함수를 포함합니다.

전역으로 상태를 관리함으로써 컴포넌트 간의 데이터 공유가 용이해지고, 중앙 집중적으로 상태를 관리할 있습니다.

(여기서 만든 store을 provider에 사용함으로써 provider의 하위 컴포넌트에서 전역으로 상태를 관리할 수 있게 됩니다.)

 

action : action은 state를 변경할 때 dispatch와 함깨 사용하는 객체입니다. (일반적으로 액션 객체라고 부릅니다)

(액션 생성자 함수를 통해 액션 객체를 반환하며, 액션 생성자 함수는 밑에서 추가로 설명하겠습니다.)

 

reducer : 이전 상태와 액션 객체를 받아서 새로운 상태를 반환하는 함수입니다. reducer는 순수 함수여야하며, 불변성을 지켜야합니다.

(자세한 내용과 사용방법은 아래에 있습니다)

 


리덕스 툴킷의 구조를 만들어보겠습니다

 

1. store만들기

provider에 넣어서 관리를 하기위한 store를 만듭니다 

 1-1. configureStore를 통해 store만들기

 1-2. reducer를 넣어주기 (지금은 리듀서가 없습니다.! 나중에 와서 넣어줄껍니다..)

 


 

2. provider 만들기

Provider로 감싸줍니다. 이전에 만든 store를 넣어 위와 같이 감싸주면,그 안에 있는 컴포넌트들이 store를 이용할 수 있게 됩니다. 


3. slice 만들기

- slice란 Redux store의 상태(state), action, reducer를 하나의 모듈로 묶어서 관리하기 위한 개념입니다.

 여기서 만든 slice는 configureStore 로 만든 store의  상태를 관리하기 위한 모듈입니다.

- createSlice를 사용하여 slice를 생성할 수 있습니다.

(createSlice 사용하면 리덕스 툴킷이 자동으로 리듀서 함수를 생성하는데, 불변성을 지키기 위해 immer를 사용하여 내부적으로 리듀서 함수를 추가합니다.. 위에서 말한 불변성을 지키기 위한 코드를 작성하지 않아도 됩니다.)

name : slice의 이름,

initialState : state의 초기값,

reducers : reducers 에 선언한 함수들은 각각 actions, reducer 프로퍼티에 들어갑니다.

(slice.actions,  slice.reducer 등으로 사용합니다. 위에선 actions를 구조분해할당으로 사용하기 편하게 작성했습니다)

 

actions : actions는 reducers에 있는 함수들을 통해 액션 생성자 함수를 생성해 가지고 있습니다. 

actions 에있는 increment, decrement, incrementByAmount 등(액션 생성자 함수) 를 이용해서 액션 객체를 생성하며, 액션 객체를  dispatch 와 같이 사용해서 state를 변환합니다.

 

reducerreducer에는 slice의 전반적인 내용이 담겨있습니다. (reducers와 다릅니다!)

(name, initialState, reducer등. 각종 자료가 담겨있습니다.)

 

여기서 만든 createSlice 를 통해 만들어진 actions, reducer를 이제 각각 사용해보겠습니다.


4. 다시store로 !

여기서 이제 configureStore 의 reducer: 부분에서 createSlice로 만든 reducer(counterReducer) 를 여기서 사용합니다.

 

(counterReducer 로 넘어온 이유는 counterSlice.reducer 가 counterReducer입니다,

export defalut로 counterReducer를 넘겼습니다. counterSlice를 넘겨  아래와 같이 사용할 수 있으나. 권장하지 않습니다.)


 

configureStore에서 reducer를 직접 만들지않고 createSlice로 만든 reducer를 쓰는 이유는 

slice의 reducer는 내부적으로 immer를 통해 불변성을 유지해주면서 새로운 상태를 반환 해주는 리듀서를 만듭니다.

 

createSlice 사용하지 않고 직접 reducer 함수를 만들어야 한다면, 해당 함수 안에서 상태를 변경하는 코드를 작성할 매번 새로운 객체를 생성하고 기존의 상태와 새로운 값을 합쳐서 반환해주어야 불변성을 유지할 있습니다. 이러한 작업은 번거롭고 실수하기 쉬운 작업이기 때문에, createSlice 사용하면 해당 부분을 자동으로 처리해줌으로써 코드 작성을 쉽고 간단하게 만들어줍니다.

 

정리하면

slice의 reducer 안에는 immer를 사용해 만든 reducer 함수와, slice의 기본적인 정보(이름, 초기 상태값)등이 들어있습니다. 그 정보를 하나의 객체 안에 담아서 사용하는 것 입니다.

reducer : {

      counter : counterReducer

}

위의 코드는  counter라는 state를 정의하고, 그 state를 관리하기 위해 reducer(counterReducer) 라는 함수를 사용한다는 의미입니다.

 

설명이 이상하지만 저런식으로 이해하시면 됩니다.


5. 실제 사용 부분

1. useSelector() 를 이용해서 store에 들어있는 값을 가져옵니다.

2. state.counter.value는 상태값을 의미하며 count에 넣었습니다.

3. useDispatch() 를 통해서 dispatch를 사용합니다. 

4. createSlice를 통해 만든 counterSlice.actions(액션 생성자 함수)를 구조분해 할당해서 가져왔고 dispatch에서 사용합니다.

(dispatch는 액션 객체를 받아서 리듀서 함수를 호출합니다)

5. dispatch에는 액션 객체가 들어가기 떄문에 액션생성자를 사용합니다.

increment, decrement의 경우 액션 생성자를 통해 액션 객체를 생성합니다.

(액션 객체 안에는 typepayload 프로퍼티가 있습니다. 그외에 다른 프로퍼티를 추가로 포함시킬 수 있으나 권장하지않음)

액션 객체의 type을 통해서 type에 맞는 리듀서 함수를 호출합니다.

 

별도의 값이 필요하지않은 리듀서 함수는 첫번째 인자인 state만 받아 사용합니다.

 

 

하지만 incrementByAmount 경우, dispatch(incrementByAmount(5))처럼 인자로 5 넘겨서 호출하면, 5만큼 증가하도록 액션 객체가 생성됩니다. (여기서 5가 payload 에 해당됩니다)

payload가 필요한 incrementByAmount 라는 리듀서 함수는 두번째 인자로 action객체를 받습니다

action객체의 payload를 통해 입력한 값을 상태에 적용합니다.

 

 

 

 


리덕스 툴킷을 공부하며..

 

저는 리덕스 툴킷에 대해 공부하면서 처음 접했을때 이 구조가 쉽게 이해 되지 않았습니다.

거의 3~4시간이 넘게 이해한게 맞는지 찾아보고, 시도해보고, AI를 이용해서 검증도 했습니다.

그래서 제가 이해하기 쉬운대로.. 이해한 그대로 작성해봤습니다.

 

정확하지 않을 수 있습니다.  chatGpt를 통해 많이 물어보고 이해한게 맞다는 답을 들었습니다. 하지만 chatGPT가 틀렸을 수 있습니다.

 

그래도 지금까지 제가 사용하면서 오류가 난 부분은 없습니다.

 

혹시라도 틀렸다면 언제든지 지적해주시면 감사하겠습니다.

 

 

'React > React Library' 카테고리의 다른 글

드래그 라이브러리 react-beautiful-dnd에 대해  (0) 2023.07.01

+ Recent posts