코알라 문제 해결

·2022년 3월 21일
0

#1 - 실행 에러

https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file

AllowJs: true를
AllowJs: false로 바꾸니 해결됐다.

자세한 내용은 해당 링크를 참고.

#2 - 팀원 작업 진행

팀원이 작업한 것을 내가 이어서 진행하게 됐다.

회원 가입 창에서 이메일, 비밀번호가 형식에 올바르지 않으면 페이지에서 바로 피드백을 주고 싶다. 이러한 것을 마이크로 인터랙션이라고 한다.
마이크로 인터랙션이란 말 그대로 아주 세세한 단위의 상호작용이다. 회원 가입 버튼을 눌렀을 때 "형식이 맞지 않습니다."라는 알림창을 띄우는 것보다, 버튼을 누르기 전에 미리 알려주면 유저는 보다 더 편리한 사용 경험을 얻는다.

기존 코드.

우선 기본 형태만 보이도록 변경.

해당 양식을 검사하는 코드를 만들자.

input이 들어오면 해당 input이 어느 타겟에서 이뤄졌는지 switch문으로 검사한다.

  • 해당 타겟마다 input값을 변경하는 set함수
  • input값이 양식에 맞는지 검사하는 check함수

두 가지를 같은 level에서 실행시킨다.

양식을 검사하는 정규식

우선 콘솔로그로 간단히 확인해보자.

잘 된다.

maxLength도 추가했다.
최대 20글자까지만 입력하라고 제한하는 것.

#3 - 콘솔 에러

콘솔창을 보니 뭔가 있다. 나는 여백의 미를 좋아한다. 해결해보자.
"password가 form 안에 들어있지 않습니다."
form 태그로 감싸라는 얘기다.

하나를 해결하면 또 뭔가가 발생하는 게 개발.

"Input 엘리먼트에는 자동완성 속성(autocomplete)을 주세요."
autocomplete="new-password"는 새로운 비밀번호, 비밀번호 확인 input에 집어넣길 권장되는 속성이다. 그 외에도 여러 속성이 있는데 MDN을 참고하자.

전부 지정해주면 해결된다.

#4 - 태그 에러

validateDOMNesting에러가 떴다. 영어를 해석하면 DOM 중첩 구조를 확인하라는 얘기 같다.

"div 태그는 p 태그의 자손으로 지정 못합니다."

p태그 안에 div태그를 넣어서 발생한 문제로 보인다. 해당 에러가 뜬 곳을 확인하자.

실제로도 p 태그 안에 div 태그가 삽입된 상태였다.
p 대신 div로 적용해서 해결했다.

이제 콘솔 말고 경고문구를 화면에 띄워보자.

우선 경고 메시지를 띄우는 컴포넌트를 분리해서 만들었다.

아까의 컴포넌트에서 내가 보고 싶은 것은 "회원 가입 양식"이지, 에러 처리를 보고 싶은 건 아니기 때문이다.

#5 - 렌더링 에러

인풋에 글자 입력했더니 무한 렌더링이 발생했다.

해당 에러 검색

I suspect that the problem lies in the fact that you are calling your state setter immediately inside the function component body, which forces React to re-invoke your function again, with the same props, which ends up calling the state setter again, which triggers React to call your function again.... and so on.
함수 컴포넌트 내부에서 setState를 바로 부르기 때문에 발생하는 문제로 보이네요.
리액트가 같은 컴포넌트를 부르고, 같은 props를 가져오고, 같은 setState를 부르고, 다시 함수 호출을 반복하고...

이렇게, 컴포넌트 내부에서 바로 setState를 실시하면 똑같은 에러가 난다. 무한 렌더링 현상.

리액트에서 무한 렌더링을 유발하는 3가지

해당 링크를 먼저 참고하자.

If you update the state directly inside your render method or a body of a functional component, it will cause an infinite loop.
클래스형 컴포넌트의 render, 혹은 함수형 컴포넌트 내부에서 직접 setState를 쓰면 무한 루프에 빠질 수 있습니다.

State updates → triggers re-render → state updates → triggers re-render → …

Do you want to update a state only one time when the component mounts? Use useEffect with an empty array as a dependency.
컴포넌트가 생성(mount)될 때만 state를 업데이트하고 싶다면 useEffect를 사용하세요.

useEffect를 써도 뭔가 에러가 난다.
살펴보자.

React Hook useEffect contains a call to 'setMessage'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [target, status] as a second argument to the useEffect Hook.
리액트훅 useEffect에서 'setMessage'를 부르는 문구를 포함하고 있습니다. 의존성 없이 이렇게 작성하면 무한 업데이트 현상이 발생할 수 있습니다.
이를 방지하려면 [target, status]를 useEffect의 두 번째 인자로 넘겨주세요.

useState, useEffect로 무한 리렌더링 방지

Changing state will always cause a re-render. By default, useEffect always runs after render has run. This means if you don't include a dependency array when using useEffect to fetch data, and use useState to display it, you will always trigger another render after useEffect runs.
Unless you provide useEffect a dependency array.
state 변경은 항.상. 리렌더링을 유발합니다.
useEffect는 항.상. 렌더가 끝난 뒤에 실행되고요.
이게 뭘 뜻하냐면, 만약 useEffect로 데이터를 가져올 때 의존성 배열을 안 쓰고 useState도 쓰고 있다면, useEffect가 끝난 뒤에도 또 렌더링이 일어날 겁니다.
useEffect에 의존성 배열을 집어넣지 않는 이상, 계속.

말끔하게 해결됐다.

리액트 생명 주기 다이어그램

useEffect를 컴포넌트 내부에서 선언하는 이유는 그래야 props, state에 접근할 수 있기 때문이다.

useEffect는 함수형 컴포넌트 내부의 함수라고 볼 수 있다. js에서 함수 내부에서 함수를 쓴다는 건 클로저가 생성된다는 이야기이다.

렌더링이 끝나고 컴포넌트 생명 주기가 끝나도 useEffect는 컴포넌트 스코프에 있는 것들을 사용할 수 있는 것.

다만 useEffect는 렌더링할 때마다 매번 새로운 effect로 교체된다고 한다.

해결은 됐지만 에러가 뜰 때마다 위아래로 왔다갔다하는 게 보기 좋지는 않다. 적당한 여유 공간을 미리 주는 게 낫겠다.

적당해진 것 같다.

#6 - 리렌더

문제가 남아있다. 렌더링이다.
위 짤에서 봤겠지만 input 하나 입력할 때마다 뭔가 굉장히 깜빡인다.

에러가 없어도 렌더링이 엄청 된다.

언제 memo를 쓸 것인가

같은 props로 변경이 자주 일어나는 상황이면 memo를 권하는 듯하다.

에러 메시지 컴포넌트는 해결됐다.

추가로 모든 입력이 정상적으로 들어올 때에만 버튼이 클릭 가능하도록 변경했다.

#7 - 라우팅

리액트 라우팅 튜토리얼 문서

페이지를 만들었으니 라우팅으로 각 페이지를 확인해봐야겠다. 일단 시키는대로 하자.

npm install react-router-dom@6

Connect the URL

First things first, we want to connect your app to the browser's URL: import BrowserRouter and render it around your whole app.
Nothing changes in your app, but now we're ready to start messing with the URL.

"URL을 연결하세요.
가장 먼저 할 일입니다. 어플에 브라우저 URL을 연결할 거예요.
BrowserRouter를 import하고 어플 전체를 감싸서 렌더링합시다.
뭔가 바뀌진 않았네요. 하지만 URL과 장난칠 준비는 됐습니다."

index.js, 혹은 main.jsx에서 작업하면 된다.

Open up src/App.js, import Link and add some global navigation. Side note: don't take the styling too seriously in this tutorial
Go ahead and click the links and the back/forward button. React Router is now controlling the URL!
We don't have any routes that render when the URL changes yet, but Link is changing the URL without causing a full page reload.

링크를 추가하세요.
src/App.js을 열고 Link를 import하세요.
그리고 전역 네비게이션을 추가해봅시다.
첨언: 튜토리얼이니 스타일을 많이 꾸밀 필요 없습니다.
실행하고 링크를 클릭해봅시다. 이제 리액트 라우터가 URL을 제어하고 있네요!
아직은 URL이 변경되어도 렌더링이 일어나지 않습니다. Link는 페이지 리로드 없이 URL을 바꾸고 있어요.

Add Some Routes

Add a couple new files:
src/routes/invoices.jsx
src/routes/expenses.jsx
(The location of the files doesn't matter, but when you decide you'd like an automatic backend API, server rendering, code splitting bundler and more for this app, naming your files like this way makes it easy to port this app to our other project, Remix 😉)
Now fill 'em up with some code:
Finally, let's teach React Router how to render our app at different URLs by creating our first "Route Config" inside of main.jsx or index.js.
경로 추가.
파일을 추가해보죠.
(파일의 위치는 중요하지 않습니다. 다만 쓰려는 앱이 자동 백엔드 API, 서버 렌더링, 코드 분할 번들링을 생각한다면 저런 식으로 작성해두면 이 앱을 다른 프로젝트로 옮길 때 편리합니다.)
이제 코드를 채워봅시다.
마무리로는 index.js에 라우트 설정을 만들어서 리액트 라우터에게 각 URL을 어떻게 렌더링할지 알려줍시다.

됐다.

#8 - 코드 재정비

그냥저냥 무난한 화면이다. 디자인이 아쉽긴 하지만, 예쁜 UI를 어떻게 꾸밀 수 있을지는 아직 모르겠다. 우선 고치고 싶은 점이 뭔지 짚어보자.

  • 현재진행중 부분에서 프로필 CSS가 엇나가는 중
  • 이미지들 간 여백
  • 그리드처럼 격자 형태로, 일정한 크기로 나열하고 싶음

key 조정

    <S.Main>
      <S.MeetingTitle>현재진행중</S.MeetingTitle>
      <S.Book>
        <S.BookCover src={bookCover} alt="BookCover" />
      </S.Book>
      <S.Members>
        {members.map((member, idx) => (
          <S.Member key={idx}>
            <S.UserInfo>
              <S.Profile src={member.profile} />
              {member.nickname}
            </S.UserInfo>
            <S.Comment>{member.comment}</S.Comment>
          </S.Member>
        ))}
      </S.Members>
    </S.Main>
  • 문제
    listkey 값을 index로 줬다.

  • 발생 가능한 이슈?
    정렬할 일이 없고 순서가 중요한 list 가 아니라서 아직은 발생할 문제가 별로 없어보인다. 하지만 굳이 index로 둘 필요는 없어보인다.

  • 대안
    index 대신 idkey를 부여한다.

        {members.map((member) => (
          <S.Member key={member.id}>
            <S.UserInfo>
              <S.Profile src={member.profile} />
              {member.nickname}
            </S.UserInfo>
            <S.Comment>{member.comment}</S.Comment>
          </S.Member>
        ))}

id 값으로 처리했다. listkey 값은 형제 노드끼리만 유니크하면 되므로 id로 해도 무방할 것이다.

화면 위치 조정

  • 원인
    닉네임이 길어지면 프로필 전체 칸이 움직이기 때문에 프로필 이미지도 엇나간다.

  • 대안
    닉네임 길이 제한

닉네임 칸 너비를 제한하는 코드 추가.

코멘트 위치 조정

코멘트의 높이가 보기 좀 이상하다.

  • 원인
    박스의 중앙에 위치하기 때문.

  • 대안
    프로필 이미지와 중앙선을 맞춰본다.

검색어 검색

검색어를 입력하면 결과물이 나오도록 하고 싶다.

// useDebounce.ts

import { useEffect, useState } from "react";

const useDebounce = <T>(value: T, delay?: number) => {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay || 200);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
};

export default useDebounce;

커스텀 훅 useDebounce.ts 를 만들어 사용.

// 커스텀 훅을 사용하는 컴포넌트

const [inputWord, setInputWord] = useState<string>("");
const debouncedValue = useDebounce<string>(inputWord);

const searchByKeyword = (totalList: IProps["topics"], keyword: string) => {
  return totalList.filter((meeting) => meeting.title.includes(keyword));
  };

// 입력창은 즉각 반응하기 위해 바로 setInputWord
const handleInput = (event: ChangeEvent<HTMLInputElement>) => {
  setInputWord(event.target.value);
};

useEffect(() => {
  const data = searchByKeyword(meetingData["topics"], debouncedValue);
  setMeetings(data);
}, [debouncedValue]);

<S.Input
	type="text"
    placeholder="찾고 있는 모임을 알려주세요."
    onChange={handleInput}
    value={inputWord}
/>

기본적으로 200ms로 적용했기 때문에 입력이 끝난 뒤에 검색한다.

추후에 초성 검색기능도 추가하면 좋을 듯하다.
중, 종성 검색은 그다지 쓸 일이 없을 듯하여 초성만 해보고 싶다.
현재 React 17로 작업중인데 다음 소규모 개인 프로젝트에선 React 18에서 새로 나온 훅으로 디바운스 적용을 해봐야겠다.

카테고리 검색

카테고리를 클릭하면 해당 카테고리를 가진 그룹을 검색하고 싶다.


for (const i in status) {
// 여기서 에러
	status[i] && array.push(i);
}

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ButtonsStatus'.
No index signature with a parameter of type 'string' was found on type 'ButtonsStatus'.ts(7053)
엘리먼트가 암묵적으로 any 타입 간주되고 있습니다. 'string' 타입이 'ButtonStatus'의 index로 쓰이고 있지 않아서 그렇습니다.

원인 :

export interface ButtonsStatus {
  교양: boolean;
  컴퓨터: boolean;
  프로그래밍: boolean;
  "순수 문학": boolean;
  과학: boolean;
  비문학: boolean;
  SF: boolean;
  판타지: boolean;
  무협: boolean;
}

위와 같이 작성한 게 문제였다.

export interface ButtonsStatus {
  [key: string]: boolean;
}

이렇게 작성해주면 모든 ButtonStatus 타입은 key값이 string으로 간주되기 때문에 해결된다.
위 코드에선 각 key 값들이 "교양": boolean, "컴퓨터": boolean이라는 제각각의 고유한 타입으로 간주되고 있었다.

  const handleClick = (e: any) => {
    const name: keyof typeof buttonsStatus = e.target.innerText;
    const newStatus = { ...buttonsStatus };
    newStatus[name] = !newStatus[name];
    handleCategory(newStatus);
  };

위와 같이 카테고리를 클릭하면, 그 버튼의 상태만 바꾸는 코드를 짰다.

모임들을 보여줄 컴포넌트에 모임들 정보를 건네주려고 했더니 타입 에러가 났다.

위와 같이 타입을 지정해주니 해결됐다.

profile
모르는 것 투성이

0개의 댓글

관련 채용 정보