[React]React를 입문해보자! (3)

DongEun·2022년 8월 25일
3
post-thumbnail

저번 이후로 뒤늦은 업로드
블로그 작성이 너무 어려워,,


1. TodoList를 제작해보자!

저는 앱처럼 보이고 싶어서 todolist를 감싸는 부분을 모바일형으로 디자인을 해뒀어요 안 하셔도 상관은 없어요.
(진짜 어플안에 들어가면 쓸모없는 짓이에요,,)


1-1. 화면부터 구상을 하자!!

제가 생각하는 구상대로 JSX를 작성하면 이런 형식이 될 거 같아요

<TodoWrap> {/* 앱 컨테이너 */}
	<Title /> {/* ToDoList */}
	<TodoListsWrap> {/* 할 일 목록 컨테이너 */}
		<TodoListCount /> {/* 체크가 안된 갯수 판별 */}
        <ListsWrap>
          <TodoListItem /> {/* 아이템이 있을경우 */}
          <TodoListNone /> {/* 아이템이 없을경우 */}
        </ListsWrap>
      <TodoButton /> {/* 입력창 (추가 버튼) */}
	</TodoListsWrap>
	<TodoInput /> {/* 입력창 (추가 , 수정) */}
</TodoWrap>



2. 데이터를 추가해보자!

2-1. 항목 데이터

 {
	id: 1,
	item: '아이템',
	checked: false,
 }

id, text, checked 세가지의 key를 가진 객체로 구성했습니다.

저는 벨로퍼트의 블로그를 참고하여서 ContextApi를 이용하여 TodoContext.js를 제작하여 todosList를 제어하는 파일을 제작했어요 파일이 분리가 되어있어서 부모에서 자식으로 이동할때 불필요한 데이터를 props를 전달하고 싶지않았거든요

TodoContext.js

import React , {createContext , useReducer  , useContext } from 'react'


const TodoStateContext = createContext(null);
// TodoLists state를 관리하는 TodoStateContext
const TodoControllContext = createContext(null);
// TodoLists를 컨토를하는 TodoControllContext


const TodosLists = [
  // {id:1 , item: "todoList" , checked: false},
];

function todoReducer(TodoState, action) {
  switch (action.type) {
    case 'CREATE':
      return TodoState.concat(action.todo);
    case 'MODIFY':
      return TodoState.map(todo =>
        todo.id === action.todo.id ? { ...todo, item: action.todo.item } : todo
      );
    case 'CHECK':
      return TodoState.map(todo =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
      );
    case 'REMOVE':
      return TodoState.filter(todo => todo.id !== action.id);
    default:
      return TodoState;
  }
}

export function TodoContext({ children }) {
  const [TodoState, dispatch] = useReducer(todoReducer, TodosLists);
  

  return (
    <>
      <TodoStateContext.Provider value={TodoState}>
        <TodoControllContext.Provider value={dispatch}>
          { children }
        </TodoControllContext.Provider>
      </TodoStateContext.Provider>
    </>
  );
}

export function useTodoState() {
  return useContext(TodoStateContext);
}
export function useTodoController() {
  return useContext(TodoControllContext);
}

useReducer를 이용하여 전역으로 state를 관리 할 수 있게 해두었고 그에따른 액션들도 todoReducer안에 정의했어요

App.js

function App() {
  return (
    <>
      <AppWrap> 
        <Header />
        <Todo/> {/* todo */}
      </AppWrap>
    </>
  );
}

저는 모바일처럼 디자인한 AppWrap에 header와 todo파일을 호출을 하였고

Todo.js

function Todo() {

  return (
    <TodoContext>
      <TodoWrap>
        <Title>ToDoList</Title>
        <TodoListsWrap>
          <TodoLists />
        </TodoListsWrap>
        <TodoInput />
      </TodoWrap>
    </TodoContext>
  );
}

todo 안에 전역으로 관리를 할 수 있게 상위에 todoContext를 작성했어요
이제 데이터를 호출만 하면 될 텐데요

TodoContext.js

const TodosLists = [
   {id:1 , item: "todoList" , checked: false},
];

TodoList.js

function TodoList() {
  const todos = useTodoState();
  // 추가 , 삭제 , 변경 된 체크값을 재배열함
  const unChecked = todos.filter((todo) => !todo.checked);

  return (
    <>
      <TodoListCount>할 일이 {unChecked.length}개 남았습니다~</TodoListCount>
      <ListsWrap>
        {todos.length > 0 ? (
            todos.map((todo) => (
              <TodoListItem
                key={todo.id}
                id={todo.id}
                item={todo.item}
                checked={todo.checked}
              />
            ))
        ) : (
          <TodoListNone>할 일을 추가 해주세요~</TodoListNone>
        )}
      </ListsWrap>
      <TodoButton />
    </>
  );
}

TodoContext에 state 값이 정의되었던 useTodoState()를 todos에 넣고
삼향 연산자로 todos 내부에 값이 있으면 상단의 TodoListItem을 노출할 것이고 그게 아니라면 TodoListNone 을 노출하게 될 거예요


2-2. 항목추가

데이터를 추가해주기 전에 input 창을 모달로 만들었기에 우선 모달창을 열어주는 거부터 해야해요

Todo.js

function Todo() {
  const [isModal, setModal] = useState({
    bool: false,
    type: null,
    listID: null,
  });

  const openModal = (id) => {
      setModal({
        bool: true,
        type: "CREATE",
        listID: null,
     });
  };

  const closeModal = () => {
    // 모달을 닫았을 경우 값 초기화
    setModal({
      bool: false,
      type: null,
      listID: null,
    });
  };

  return (
    <TodoContext>
      <TodoWrap>
        <Title>ToDoList</Title>
        <TodoListsWrap>
          <TodoLists openModal={openModal} />
        </TodoListsWrap>
        <TodoInput isModal={isModal} close={closeModal} />
      </TodoWrap>
    </TodoContext>
  );
}

useState로 isModal의 값을 bool(창이 열렸는지 여부) , type(수정인지 추가인지 여부) , listID(수정이면 Item의 id)로 관리를 하였고

props로 메소드를 전달했어요
(이부분도 전역으로 하고싶어서 react potal이랑 context를 적용할까 싶었는데 파일이 너무 커지는거 같아서 나중에 하려구요,,)

그 후에 인풋 버튼이 있는 TodoLists에 openModal을 정의 해주고

TodoLists.js

function TodoList({ openModal }) {
 const todos = useTodoState();
 // 추가 , 삭제 , 변경 된 체크값을 재배열함
 const unChecked = todos.filter((todo) => !todo.checked);

 return (
   <>
     <TodoListCount>할 일이 {unChecked.length}개 남았습니다~</TodoListCount>
     <ListsWrap>
       {todos.length > 0 ? (
           todos.map((todo) => (
             <TodoListItem
               key={todo.id}
               id={todo.id}
               item={todo.item}
               checked={todo.checked}
               openModal={openModal}
             />
           ))
         // listComponents
       ) : (
         <TodoListNone>할 일을 추가 해주세요~</TodoListNone>
       )}
     </ListsWrap>
     <TodoButton onClick={openModal} />
   </>
 );
}

추가 버튼을 하는 TodoButton에 openModal이벤트를 호출을 했고
수정 버튼이 있는 TodoListItem에도 openModal을 또 내려주었어요
(이부분이 마음에 안들어서 수정을 하고 싶은데 좀 더 찾아봐야겠네요)

TodoInput.js

function TodoInput({ isModal, close }) {

  return (
    <>
      {
        isModal.bool && (
          <ModalWrap>
            <TextWrap>
              <TextField onSubmit={onSubmit}>
                <input
                  id="todoText"
                  type="text"
                  placeholder="할 일을 적어주세용!"
                  autoFocus // 팝업창이 열리면 자동으로 인풋에 포커스가 가도록 설정
                />
                {/* 버튼을 컴포넌트화 하여 이벤트 전달 */}
                <StyleButton btnType="submit" buttonText="저장" />
                <StyleButton
                  btnType="button"
                  buttonText="취소"
                  clickEvent={close}
                />
              </TextField>
            </TextWrap>
          </ModalWrap>
        )
      }
    </>
  );
}

그렇게 isModal의 bool 값이 true일 경우에 ModalWrap을 생성 해주고
StyleButton(button)에 clickEvent(onClick)에 close 이벤트를 선언 해주었어요

여기 까지만 해두면 인풋창이 모달로 나올텐데 이제 본론으로 추가 버튼 기능을 추가 해볼거에요

TodoInput.js

function TodoInput({ isModal, close }) {
  const [inputValue, setValue] = useState(""); 		// inputValue을 useState로 관리
  const onChange = (e) => setValue(e.target.value); // input의 value가 변할때마다 inputValue에 state값 변경
  const todos = useTodoState();						 // TodoContext.js에 선언한 TodoLists를 가져옴
  const dispatch = useTodoController();				 // TodoContext.js에 선언한 todoReducer에 보내는 역할
  const nextId = useRef(todos.length); 				// 생성할때마다 다음 번호는 todos의 갯수의 다음번호로 설정

  const onSubmit = (e) => {
    e.preventDefault(); // form의 이벤트를 막기위해서 추가
    if (inputValue.length <= 0) { // input의 값이 없을경우 이벤트 막기
      alert("할 일을 적어주세용~");
      return false;
    }
    let TodoID = nextId.current;

    dispatch({
      type: "CREATE",
      todo: {
        id: TodoID,
        item: inputValue,
        checked: false,
      },
    });
    setValue("");
    close();
  };

  return (
    <>
      {
        isModal.bool && (
          <ModalWrap>
            <TextWrap>
              <TextField onSubmit={onSubmit}>
                <input
                  id="todoText"
                  onChange={onChange}
                  value={inputValue}
                  type="text"
                  placeholder="할 일을 적어주세용!"
                  autoFocus
              </TextField>
            </TextWrap>
          </ModalWrap>
        )
      }
    </>
  );
}

TodoContext.js

function todoReducer(TodoState, action) {
  switch (action.type) {
    case 'CREATE':
      return TodoState.concat(action.todo);
    default:
      return TodoState;
  }
}

TodoInput에서 dispatch로 TodoContext로 데이터를 보내는데 그렇게 받은 데이터를 action의 타입을 확인하여 CREATE일 경우 todoState에 concat으로 배열을 합쳐요

3. 데이터를 삭제해보자!

TodoListItem.js

function TodoListItem({ id, item, checked, openModal }) {
  const dispatch = useTodoController();

  const onRemove = (e) => {
    dispatch({
      type: "REMOVE",
      id,
    });
  };

  return (
    <>
      <CheckBoxList>
        <CheckBox>
          <input
            id={`List${id}`}
            type="checkbox"
          />
          <label htmlFor={`List${id}`}>{item}</label>
        </CheckBox>
        <StyleButton
          btnType="button"
          buttonText="삭제"
          clickEvent={() => onRemove(id)}
        />
      </CheckBoxList>
    </>
  );
}

삭제 버튼을 추가해준 후에 onRemove를 추가해주고 dispatch로 해당 아이템의 id값을 인자값으로 올려주었어요

TodoContext.js

function todoReducer(TodoState, action) {
  switch (action.type) {
    case 'CREATE':
      return TodoState.concat(action.todo);
    case 'REMOVE':
      return TodoState.filter(todo => todo.id !== action.id);
    default:
      return TodoState;
  }
}

그러면 todoReducer에서 추가와 똑같이 action.type을 읽어오고 filter로 그 값을 빼주었어요


4. 데이터를 수정을 해보자!

Check의 유무와 텍스트값이 변경이 되어야 한다 생각하고 수정을 할때 map함수로 정의를 해 줄 수 있다는걸 찾아봤어요 그래서 저도 map을 이용해서 데이터에 접근해서 수정할 예정이에요

4-1. Check의 유무

TodoListItem.js

<CheckBox>
  <input
  id={`List${id}`}
  type="checkbox"
  checked={checked}
  onChange={onChecking}
  />
  <label htmlFor={`List${id}`}>{item}</label>
</CheckBox>

TodoContext.js

function todoReducer(TodoState, action) {
  switch (action.type) {
    case 'CREATE':
      return TodoState.concat(action.todo);
    case 'CHECK':
      return TodoState.map(todo =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
      );
    case 'REMOVE':
      return TodoState.filter(todo => todo.id !== action.id);
    default:
      return TodoState;
  }
}

TodoContext에 case로 Check를 추가해 주었구 불러온 map에 checked: !todo.checked 으로 true , false를 변경해 주었어요 이 방식을 이용해서 item의 텍스트도 똑같이 변경할 예정이에요

4-2. 수정하기 버튼을 만들자!

위에서 모달창으로 연결을 해둔곳에 이제 추가와 함께 수정 case를 제작할거에요 우선 todo.js에 모달을 열때 변화를 줄것이고 그거로 dispatch로 호출할겁니당

Todo.js

function Todo() {
  const [isModal, setModal] = useState({
    bool: false,
    type: null,
    listID: null,
  });

  const openModal = (id) => {
    // 모달을 열때 id값이 있을경우에 id값을 인자값으로 넘기고 isNaN으로 숫자인지 판별후 
    // id가 숫자일경우 MODIFY로 넘기고 없을경우에는 CREATE로 작성
    if (!isNaN(id)) {
      setModal({
        bool: true,
        type: "MODIFY",
        listID: id,
      });
    } else {
      setModal({
        bool: true,
        type: "CREATE",
        listID: null,
      });
    }
  };

TodoInput.js

function TodoInput({ isModal, close }) {
  const [inputValue, setValue] = useState("");
  const onChange = (e) => setValue(e.target.value);
  const todos = useTodoState();
  const dispatch = useTodoController();
  const nextId = useRef(todos.length);

  useEffect(() => {
    //useEffect로 isModal의 값이 변했을때 MODIFY일 경우 인풋에 item을 적어주는 역할
    if (isModal.type === "MODIFY") {
      setValue(todos.map((todo) => {
        return todo.id === isModal.listID ? todo.item : "";
      }));
    }
    //useEffect로 isModal의 값이 변했을때 false일 경우 인풋에 item을 초기화 하는 역할
    if (isModal.bool === false) {
      setValue("");
    }
  }, [isModal , todos]);

  const onSubmit = (e) => {
    e.preventDefault();
    if (inputValue.length <= 0) {
      alert("할 일을 적어주세용~");
      return false;
    }
    let TodoID = nextId.current;
    if (isModal.type === "CREATE") {
      TodoID = nextId.current++; //nextId 1씩 더하기
    } else if (isModal.type === "MODIFY") {
    	//MODIFY일경우 기존의 아이디값을 넣어주기
      TodoID = isModal.listID;
    }

    dispatch({
      type: isModal.type,
      todo: {
        id: TodoID,
        item: inputValue,
        checked: false,
      },
    });
    setValue("");
    close();
  };
}

TodoContext.js


function todoReducer(TodoState, action) {
  switch (action.type) {
    case 'CREATE':
      return TodoState.concat(action.todo);
    case 'MODIFY':
      return TodoState.map(todo =>
        todo.id === action.todo.id ? { ...todo, item: action.todo.item } : todo
      );
    case 'CHECK':
      return TodoState.map(todo =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
      );
    case 'REMOVE':
      return TodoState.filter(todo => todo.id !== action.id);
    default:
      return TodoState;
  }
}

이렇게 예외 처리를 하면서 추가와 수정을 함께 작성했어요
이렇게 해서 CRUD 기능을 갖춘 TODOLIST를 제작해봤는데
사이트를 제작하는것보다 블로그에 어떻게 정리를 할지 더 오래걸린거같아요,,,

자료를 공유해주시는 모든분들 감사합니다!!

reference

profile
다채로운 프론트엔드 개발자

0개의 댓글