[TIL] 내일배움캠프 React 과정 2022.12.15

Today I Learned

  • React 숙련 개인 과제 수행

 


Redux로 TO DO LIST 구현하기

styled-components 사용하기

const Btn = styled.button`
    margin-right: 12px;
    height: 35px;
    width: 120px;
    border-radius: 20px;
    border: transparent;
    color: white;
    font-size: 16px;
    font-weight: bold;
    cursor: pointer;
    background-color: ${(props) => props.backgroundColor};

    :hover {
        opacity: 0.8;
    }
`

styled-components의 장점을 잘 보여주는 예시 코드. jsx에서 props를 넘겨받아서 조건부 스타일링이 가능하다! 나는 추가, 삭제, 완료 버튼 등의 디자인이 동일하고 색상만 달랐기 때문에 Btn이라는 컴포넌트를 하나 만들고 props로 backgroundColor를 넘겨서 스타일링했다. 그리고 hover 같은 경우에도 스타일 컴포넌트 내에서 바로 지정하면 되어서 한눈에 파악하기 좋다.

 

 

styled-components 사용하면 class 명에 hash화 되는 문제

styled-components를 사용하면 콘솔에서 코드를 확인했을 때 class 명이 모두 hash 값으로 바뀌어서 어떤 class인지 알아보기 힘들다는 단점이 있다. 이 문제는 별도의 react 프로그램을 설치하면 해결되는 거 같다. 일단 프로그램 추가 설치는 하지 않고 방법이 있다는 것만 알아놓도록 한다.

 

튜터님이 알려주신 해결 방법이 담긴 링크

https://blog.woolta.com/categories/1/posts/198

 

 

styled-components에 미디어 쿼리 적용하기

const StForm = styled.div`
  width: 1200px;

  @media only screen and (max-width: 1200px) {
    width: 100%;
  }
`

미디어 쿼리를 원하는 스타일 컴포넌트 안에 @media only screen and (max-width: 1200px)로 코드를 작성하면 된다.

 

 

styled-components를 js 파일로 나눈 후 폰트 적용하기

@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap');

원래 CSS에서는 이렇게 @로 import하면 됐었는데 styled-components를 styled.js 파일로 나누고 난 후에 javascript 파일에서는 폰트 url을 어떻게 import해야 하는지 모르겠다.

 

검색해 보니 대부분 woff 폰트 파일을 다운 받아서 프로젝트 폴더에 넣고 @font-face로 저장해서 사용하는 거 같다. 일단은 지금 과제에서 폰트 적용하는 게 그렇게 중요한 부분은 아니기 때문에 나중에 프로젝트 진행할 때 경험해도 될 거 같다.

 

참고

https://good-potato.tistory.com/entry/React-Styled-components-web-font-적용

https://velog.io/@071yoon/Typescript-React-Styled-Component-환경에서-Font-적용하기

 

styled-components 이용해서 버튼 컬러 넘겨주기

function Button ({list}) { 

  const dispatch = useDispatch();

  // [완료] 혹은 [취소] 버튼 눌렀을 때 실행됨
  const changeDoneHandler = (id) => {
    dispatch(changeDone(id))   
  }

  // 투두리스트를 완료했을 때
  if (list.isDone === false) {
    return (
      <DoneBtn 
        onClick={() => changeDoneHandler(list.id)}
        style={{backgroundColor: '#acaaed'}}
      >완료</DoneBtn>
    );

  // 투두리스트 완료를 취소했을 때
  } else if (list.isDone === true) {
    return (
      <DoneBtn 
        onClick={() => changeDoneHandler(list.id)}
        style={{backgroundColor: '#FF9F9F'}}
      >취소</DoneBtn>
    );
  }
}

원래 TodoList 안에 Button 컴포넌트를 넣고 이 컴포넌트에서 if문으로 텍스트와 컬러를 결정했는데 styled-components의 장점을 활용해서 Button 컴포넌트를 없애고 TodoList안에서 해결하기로 했다.

 

<Btn backgroundColor={"#f9ba86"} 
onClick={() => navigate(`/edit/${todo.id}`)}>수정</Btn>     
<Btn backgroundColor={"#8EC3B0"} 
onClick={() => deleteHandler(todo.id)}>삭제</Btn>
<Btn backgroundColor={ todo.isDone ? "#FF9F9F" : "#acaaed"}
onClick={() => changeDoneHandler(todo.id)}>{ todo.isDone ? "취소" : "완료"}</Btn>

수정한 코드의 구조는 이렇다. 그리고 원래 DeleteBtn, DoneBtn, EditBtn 등 버튼 별로 css를 따로 설정했었는데 어차피 생김새는 똑같고 색깔만 다른 것이기 때문에 Btn 하나로 통일해서 backgroundColor를 props로 넘겨준다.

 

const Btn = styled.button`
    margin-right: 12px;
    height: 35px;
    width: 100px;
    border-radius: 20px;
    border: transparent;
    color: white;
    font-size: 16px;
    font-weight: bold;
    cursor: pointer;
    background-color: ${(props) => props.backgroundColor};

    :hover {
        opacity: 0.8;
    }
`

Detail.jsx에서 넘긴 backgroundColor props를 styled.js 파일에서 이렇게 받는다.

 

 

컴포넌트의 특성으로 인한 div 구조 문제

좌-문제O / 우-문제X

const Home = () => {
    return (
        <div>
            <Form />
            <TodoList isDone={false} />
            <TodoList isDone={true} />
        </div>
    )
}

Home 컴포넌트에 부모 요소를 필수로 넣어야 해서 불필요한 div를 추가하고 그 안에 Form과 TodoList를 넣다 보니 Header와 Form, TodoList가 동일 선상에 놓이지 않고 이로 인해 미디어 쿼리가 이상하게 적용되는 등의 문제가 발생했다.

 

const Home = () => {
    return (
        <>
            <Form />
            <TodoList isDone={false} />
            <TodoList isDone={true} />
        </>
    )
}

오늘 튜터님께 처음 배운 사실. 빈 태그를 쓸 수 있다! 최상위 부모 요소에 빈 태그를 넣으니 상단 이미지의 우측처럼 문제가 해결되었다.

 

 

수정하기 기능 구현하기

원래 과제 요구사항이 CRUD를 구현하는 것이었는데 예시 페이지에는 수정하기 기능이 없어서 이걸 할까 말까 고민을 했었다. 근데 생각해 보니까 [추가] 기능에 사용했던 input 값 가져오는 함수와 [취소/완료] 기능에서 특정 객체의 isDone만 수정하는 부분을 이용하면 될 거 같아서 도전해보기로 했다.

 

// input 창에 제목과 내용을 입력했을 때 입력값 가져오기
const inputContent = (e) => {
    if (e.target.name === 'title') {
      setTitle(e.target.value)
    } else if (e.target.name === 'content') {
      setContent(e.target.value)
    }
}

// [수정] 버튼 클릭했을 때 실행됨
const editHandler = () => {
    navigate("/")
    dispatch(
        // title과 content만 수정한 객체를 dispatch로 보냄
        editTodo({
          id: todo.id,
          title: title,
          content: content,
          isDone: todo.isDone,
        })
      )
}

id와 todo는 그대로 두고 input으로 받은 title과 content만 수정해서 payload로 보낸다.

 

// 작성한 투두리스트 수정하기
case EDIT_TODO:
    let copy2 = [...state];
    return (
        copy2.map((list) => list.id === action.payload.id ? action.payload : list)
    )

reducer에서 map을 돌려서 id가 일치하면 action.payload로 해당 객체만 수정한다.

 

 

input에 value 설정할 때 주의할 점

수정 전

const todos = useSelector((state) => state.todos);
const todo = todos.find((list) => list.id === param.id);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');

<input id="title" value={todo.title} name='title' method="post" onChange={inputContent} />
<input id="coneent" value={todo.content} name='content' type="text" method="post" onChange={inputContent} />

원래 있던 제목과 내용을 받아오기 위해 input value에 todo.title과 todo.contetn를 넣었더니 값이 고정되어 버려서 input 창에 텍스트를 입력을 해도 입력이 되지 않는 문제가 발생했다.

 

수정 후

const [title, setTitle] = useState(todo.title);
const [content, setContent] = useState(todo.content);

<input id="title" value={title} name='title' method="post" onChange={inputContent} />
<input id="coneent" value={content} name='content' type="text" method="post" onChange={inputContent} />

value에는 title과 content만 넣어서 setTitle로 값을 변경할 수 있게 해 주고 useState에 todo.title과 todo.content를 넣어서 기존의 title과 contetn를 가져올 수 있게 한다.

 

 

input에 autofocus 설정하기

input의 border를 모두 없애버렸기 때문에 혹시 input창이라는 것을 한눈에 알아보기 힘들까 봐 autofocus 기능이 있으면 좋겠다고 생각했다. autofocus란 수정 페이지에 들어갔을 때 제목 부분에 커서가 자동으로 깜빡이는 것이다.

 

<TitleInput autofocus />

input 태그에 autofocus 속성을 넣었는데 내가 원하는 대로 작동하지 않는다. 혹시 styled-components이기 때문에 문제가 되는 것일까 해서 'styled-components input autofus'로 검색해봤다.

 

useEffect(() => {
    document.querySelector('#title').focus()
});

구글링 하다가 useEffect가 갑자기 생각나서 useEffect를 사용하기로 했다!

useEffect는 리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook이다. -> 딱 내가 원하던 부분이다.

 

Invalid DOM property `autofocus`. Did you mean `autoFocus`?

useEffect로 수정하고 나서 뒤늦게 콘솔 창에서 에러 메시지를 발견했다. f를 대문자로 작성하면 해결되는 간단한 문제였다 ㅎ useEffect를 삭제하고 title input 속성을 autoFocus로 변경했다.

 

 

map return 제일 상위 요소에 key 넘기기

Warning: Each child in a list should have a unique "key" prop.

아마 이번 과제 진행하면서 많이들 경험했을 거 같은 에러 메시지 ㅋㅋ 이거 도대체 어떻게 해야 없어지는 건지 궁금했는데 튜터님이 알려주셨다. map을 돌렸을 때 return 값에 최상위 요소에 key를 넣어주면 된다.

 

 {todos.filter((list) => list.isDone === isDone)
                .map((list) => {
                    return (
                            <ListCard key={list.id}>
                                <Link to={`/${list.id}`}>
                                    <Detail><span>상세보기</span></Detail>
                                </Link>
                                <ListText>
                                    <TodoTitle>{list.title}</TodoTitle>
                                    <TodoContent>{list.content}</TodoContent>
                                </ListText>                        
                                <TodoBtns>
                                    {/* Btn에 props로 backgroundColor를 전달함 */}
                                    <Btn backgroundColor={"#8EC3B0"} 
                                    onClick={() => deleteHandler(list.id)}>삭제</Btn>
                                    <Btn backgroundColor={ list.isDone ? "#FF9F9F" : "#acaaed"}
                                    onClick={() => changeDoneHandler(list.id)}>{ list.isDone ? "취소" : "완료"}</Btn>
                                </TodoBtns>
                            </ListCard>
                    );
                })}

여기서는 ListCard에 prop로 key를 넘겨줘야 된다.

 

 

local에서 git commit 메시지 수정하기

git commit --amend -m "바꿀 메시지"

 

 


회고

튜터님들은 많이 괴롭혀서(?) 원하는 기능 구현에는 성공했다. 해결 못한 몇 가지 사소한 문제가 있기는 하지만 일단 과제를 제출했다. 리덕스 강의 들을 때만 해도 너무 어렵고 내가 이걸 할 수 있을까 걱정됐었는데 막상 과제를 하다 보니까 어떻게 했는지도 모르게 완성했다. 다시 처음부터 해보라고 하면 못할 거 같은 ㅋㅋ 

 

이번에는 거의 매일 튜터님들을 찾아가서 정말 해결 못하겠는 부분을 질문드렸는데 그 과정에서 내가 리덕스를 전부 이해하지 못하고 있다는 것을 깨달았고 물어보면서 배운 부분은 그냥 강의를 들었을 때보다 더 기억에 남는 거 같다. 부트캠프의 장점이 이렇게 경험 많은 튜터님들에게 도움을 받을 수 있는 부분인 거 같다. 그나저나 vercel로 배포해보려고 했는데 계속 말썽이네..