문제 발생

페이지네이션 컴포넌트를 만들면서 url 쿼리 스트링을 통해 페이징을 하였다.

처음 만들 때는 생각없이 '?' 를 붙여서 쿼리 스트링을 추가해주었지만 여러개의 쿼리 스트링을 쓸 때 문제가 생겼다.

쿼리 스트링이 없을 때와 기존에 쿼리 스트링이 1개 이상 있을 때 붙여줄 부호가 달랐다.

// 쿼리 스트링이 없을 때 -> '?'로 연결
/path?page=1

// 쿼리 스트링이 있을 때 -> '&'로 연결
/path?keyword=hi&page=1

문제 해결

URL 쿼리 스트링을 포매팅 해주는 유틸 함수를 만들어서 해결했다.

유틸함수는 사용하는 쿼리 스트링들을 객체 형식으로 key: value 값으로 받아서 쿼리 스트링으로 포매팅 한다.

이렇게 만들어진 쿼리 스트링을 기존 path에 '?'로 연결하면 되게끔 만들어뒀다.

objectToQueryString.ts

export default function objectToQueryString(obj: {
  [key: string]: any;
}): string {
  return Object.entries(obj)
    .map(
      ([key, value]) =>
        `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
    )
    .join("&");
}

문제 발생

Next.js의 페이지 라우터를 쓰고 있었고, 지금껏 개발할 때는 아무 문제가 없었다.

하지만 배포를 시작하면서 빌드를 해보니 그동안 없었던 문제가 생기기 시작했다.

에러 메시지는 아래와 같았다.

// 에러 메시지
Build optimization failed: found pages without a React Component as default export in
pages/my-shop/register/components/Input/types/types.ts
pages/signin/types/types.ts
pages/signup/types/types.ts
pages/home/hooks/userRequest.ts
pages/home/hooks/useUserQuery.ts

문제 해결

결론부터 말하자면 pages 폴더 내에 리액트 컴포넌트 이외의 파일을 만들어서 생긴 문제였다.

팀 프로젝트에서 폴더 구조 및 네이밍 컨벤션을 정할 때 아래처럼 네이밍 하기로 했다.

// 컴포넌트
/pages/{페이지 이름}/components/{컴포넌트 이름}/index.tsx  

// 타입
/pages/{페이지 이름}/types/types.ts    

 

Next.js에서는 pages 폴더 내의 파일을 페이지 컴포넌트로 읽는다.

따라서 타입 파일도 페이지 컴포넌트로 읽는데, 리액트 컴포넌트가 아니기 때문에 발생한 문제였다.

그래서 페이지 컴포넌트라는걸 명시해주기 위해 아래와 같은 설정을 추가해주고 페이지 파일의 이름을 변경했다.

// 페이지 파일 이름 변경
index.tsx -> index.page.tsx

next.config.js

const nextConfig = {
  ...
  pageExtensions: ["page.tsx", "page.ts", "page.jsx", "page.js"],
  ...
}
module.exports = nextConfig;

문제 발생

Next.js와 TS를 쓰면서 프로젝트를 진행하고 있었다.

axios로 API 요청을 하고 서버에서 내려주는 에러 메시지를 이용해 에러 핸들링을 하려고 했다.

try ~ catch 문을 이용해 error를 잡고, error 객체의 message를 이용하려고 하니 아래와 같은 컴파일 에러가 발생했다.

 

console.log를 찍어보면 error 객체는 아래처럼 분명 존재한다.

문제 해결

JS를 쓸 때는 타입 체크를 하지 않기 때문에 사용에 문제 없었지만 

TS에서는 쓰면 error가 unknown 타입이라 error 객체의 프로퍼티로 바로 접근할 수 없다.

여러 방법이 있겠지만 axios에서 제공하는 isAxiosError라는 내부 메서드를 이용했다.

일종의 조건문으로 타입 좁히기를 하는거라고 생각하면 될것 같다.

아래의 else if문처럼 try ~ catch문 안에서 특정 조건에 따라 특정 에러 객체를 반환하도록 설정하거나

혹은 axios 인스턴스 내에서 인터셉터로 에러를 던지도록 하고 catch문 내에서 분기 처리하면 여러 에러 상황을 핸들링 할 수도 있다.

 

문제 발생

eslint 와 prettier 를 병행해서 쓰려고 했다.

이 글을 보고 eslint-config-prettier 방식으로 eslint에서 prettier와 충돌할 수 있는 rule을 꺼주기로 했다.

하지만 .eslintrc.cjs / .prettierrc.cjs 를 설정하던 도중 아래와 같은 문제를 만났다.

 

문제 해결

사실 아직 익숙하지 않아서 이 방법이 완벽한 답인지는 모르겠다.

하지만 내가 이해한 바로는, .eslintrc.cjs 설정에서 plugins 에 prettier 를 넣어줬기 때문으로 보인다.

VSCode 에서 prettier를 사용하는데 굳이 plugins 에넣어야 할까? 라는 생각이 들어서 빼버렸다.

 

또한 extends 부분에 위의 plugins 에 들어있는 라이브러리의 plugin:**/recommended 를 넣으니 해결됐다.

 

일단 해결되긴 했는데 정확한 원리는 이해하지 못해서 자유자재로 쓰려면 더 공부를 해봐야할 것 같다.

문제발생

기존의 순수 JS 로 페이지를 만들다가 리액트로 넘어가려고 했다.

원격 리포지토리에서 리액트 앱 초기화만 해둔걸 로컬로 가지고 와서 npm run start 로 실행해보았다.

하지만 결과는 아래와 같았다.

 


문제해결

아직 npm 에 익숙하지 않아서 겪은 문제라고 생각한다.

우선 무엇이 문제인지 확인해보기 위해 npm ls 를 입력해보고 패키지를 확인해보았다.

아래와 같은 결과가 나왔고 어떤게 문제인지 살짝 감이 오기 시작했다.

1. npm ls 입력

라이브러리를 다운받아야하는데 npx create-react-app 로 설치했다면 문제가 없었겠지만,

깃허브에서 pull 받았기 때문에 패키지 모듈이 안깔린거 같았다.

그래서 package.json 파일에서 안쓸거같은 web-vitals 같은 라이브러리는 지우고 npm install 로 나머지 패키지를 다운받았다.

2. npm install 후 npm ls 로 재확인

패키지 모듈들도 잘 설치된걸 확인했고 실행해보니 문제가 해결됐다.

앞으로 이런 문제를 많이 만날텐데 npm 에도 익숙해져야겠다.

문제발생

페이지를 열면 데이터를 바로 불러오기 위해 아래처럼 부모 컴포넌트에서 useEffect 로

비동기 함수를 호출하여 데이터를 받아온 뒤 자식 컴포넌트로 내려주었다.

부모 컴포넌트인 App 에서 자식 컴포넌트 FolderListPage 로 folders 를 내려주어 이를 이용하려고 하였다.

console.log 로 folders.folder 의 데이터를 확인하니 아래와 같았다.

 

여기서 links 의 내용을 확인하고, LinkItem 컴포넌트로 데이터를 내려주려고 하던 중에 에러가 발생하였다.

App.js

function App() {
  const [folders, setFolders] = useState([]);
  const [userProfile, setUserProfile] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const folders = await getFolders();
        const userProfile = await getUserProfile();

        if (!folders) return;
        setFolders(folders);
        setUserProfile(userProfile);
      } catch (error) {
        console.log(error);
      }
    };
    fetchData();
  }, []);

  return (
    <>
      <Nav userProfile={userProfile} />
      <FolderListPage folders={folders} />
      <Footer />
    </>
  );
}

FolderListPage.js

function FolderListPage({ folders }) {
  const favoriteFolder = folders.folder;
  console.log(folders.folder.links);  // 이 부분에서 에러
  return (
    <div>
      <ListPage favoriteFolder={favoriteFolder}>
        <form className={searchBarStyles.form}>
          <input name="search" placeholder="링크를 검색해 보세요."></input>
          <img
            className={searchBarStyles.searchIcon}
            src={searchIcon}
            alt="검색창 아이콘"
          />
        </form>
        <div className={styles.linkList}>
          {favoriteFolder &&
            favoriteFolder.links.map((linkInfo) => (
              <LinkItem key={linkInfo.id} linkInfo={linkInfo}></LinkItem>
            ))}
        </div>
      </ListPage>
    </div>
  );
}

 

 


문제해결

첫 렌더링에는 folders 의 값이 useState 의 초기값으로 설정한 [] 이라서 생긴 문제라고 생각한다.

folders 의 값을 console.log 로 찍어보기 위해 새로고침하면 아래와 같았다.

 

비동기로 데이터를 호출하므로 첫 렌더링 이후 비동기 함수가 끝나면 값이 바인딩 되는데

첫 렌더링 시 folders 의 값은 빈 배열 []이므로 folders.folder 의 결과는 undefined 이다.

따라서 이 때  folders.folder.links 로 바로 접근하면 undefined.links 로 접근하려는 것이라서 발생한 에러인듯 하다.

그래서 ?. 연산자를 이용하여 문제를 해결하였는데, 더 좋은 방법이 있는지는 찾아봐야겠다.

FolderListPage.js

function FolderListPage({ folders }) {
  const favoriteFolder = folders.folder;
  console.log(folders.folder?.links);  // 이 부분을 ?. 연산자 이용
  return (
    <div>
      <ListPage favoriteFolder={favoriteFolder}>
        <form className={searchBarStyles.form}>
          <input name="search" placeholder="링크를 검색해 보세요."></input>
          <img
            className={searchBarStyles.searchIcon}
            src={searchIcon}
            alt="검색창 아이콘"
          />
        </form>
        <div className={styles.linkList}>
          {favoriteFolder &&
            favoriteFolder.links.map((linkInfo) => (
              <LinkItem key={linkInfo.id} linkInfo={linkInfo}></LinkItem>
            ))}
        </div>
      </ListPage>
    </div>
  );
}

문제발생

일단 요구사항에 따라 로그인 페이지를 만들어 보았다.

요구사항대로 동작은 하지만, 내가 짰어도 별로 좋은 코드는 아닌거 같았다.

리팩토링하고 싶은 욕구가 마구마구 나지만 아직 JS 에 익숙하지 않아서 코드 리뷰 받기 까지 미루었다.

그래서 코드 리뷰를 받고 설명도 들은 지금, 이전에 만들었던 로그인 페이지를 리팩토링을 해보았다.

 


문제해결

수정을 하고 나니 전반적으로 수정 이전보다 코드가 훨씬 깔끔해졌다.

이전보다 변경에 유연해졌고, 검증 로직을 추상화하여 다른 곳에서도 쓸 수 있을 정도로 재사용성이 올라갔다. 

에러메시지 출력을 input 태그의 텍스트 노드를 변경하는 방식으로 바꿔서 클래스를 추가, 제거 하는 부분도 깔끔해졌다.

코드 리뷰를 받고 수정한 내용을 정리하자면 아래와 같다.

  • 이메일 / 비밀번호 검증 로직 분리
  • 에러 메시지 출력 방식 변경

변경 전 HTML

<div>
  <label class="sign-input-label">이메일</label>
  <input
    class="sign-input-email"
    type="email"
    value=""
    placeholder="이메일"
  />
</div>
<span class="email-empty hidden">이메일을 입력해주세요.</span>
<span class="email-wrong hidden">올바른 이메일 주소가 아닙니다.</span>
<span class="not-valid-email hidden">이메일을 확인해주세요.</span>
<div class="sign-password">
  <label class="sign-input-label">비밀번호</label>
  <input
    class="sign-input-password"
    type="password"
    placeholder="비밀번호"
  />
  <button class="eye-button" type="submit">
    <img src="/images/eye-off.svg" />
  </button>
</div>
<span class="password-empty hidden">비밀번호를 입력해주세요.</span>
<span class="not-valid-password hidden">비밀번호를 확인해주세요.</span>
<div>
  <button class="signin-btn">로그인</button>
</div>

 

변경 후 HTML

<div>
  <label class="sign-input-label">이메일</label>
  <input
    class="sign-input-email"
    type="email"
    value=""
    placeholder="이메일"
  />
</div>
<span class="email-error-msg"></span>
<div class="sign-password">
  <label class="sign-input-label">비밀번호</label>
  <input
    class="sign-input-password"
    type="password"
    placeholder="비밀번호"
  />
  <button class="eye-button" type="submit">
    <img src="/images/eye-off.svg" />
  </button>
</div>
<span class="password-error-msg"></span>
<div>
  <button class="signin-btn">로그인</button>
</div>

변경 전 JS

const signinBox = document.querySelector(".signin-box");
const signinBtn = document.querySelector(".signin-btn");

const emptyEmail = document.querySelector(".email-empty");
const wrongEmail = document.querySelector(".email-wrong");
const emptyPassword = document.querySelector(".password-empty");
const notValidEmail = document.querySelector(".not-valid-email");
const notValidPassword = document.querySelector(".not-valid-password");

const emailInput = document.querySelector(".sign-input-email");
const passwordInput = document.querySelector(".sign-input-password");

const TEST_EMAIL = "test@codeit.com";
const TEST_PW = "codeit101";

function inputCheck(e) {
  const type = e.target.type;
  const value = e.target.value;
  let regex = new RegExp("[a-z0-9]+@[a-z]+.[a-z]{2,3}");

  if (type == "email") {
    if (Number(value) == 0) {
      emptyEmail.classList.remove("hidden");
      emailInput.classList.add("error");
    } else if (!regex.test(value)) {
      wrongEmail.classList.remove("hidden");
      emailInput.classList.add("error");
    }
  } else if (type == "password") {
    if (Number(value) == 0) {
      emptyPassword.classList.remove("hidden");
      passwordInput.classList.add("error");
    }
  }
}

function reset(e) {
  const type = e.target.type;

  if (type == "email") {
    emptyEmail.classList.add("hidden");
    wrongEmail.classList.add("hidden");
    notValidEmail.classList.add("hidden");

    emailInput.classList.remove("error");
  } else if (type == "password") {
    emptyPassword.classList.add("hidden");
    notValidPassword.classList.add("hidden");

    passwordInput.classList.remove("error");
  }
}

function validation() {
  const email = emailInput.value;
  const password = passwordInput.value;
  if (email == TEST_EMAIL && password == TEST_PW) {
    window.location.href = "/forder.html";
  } else {
    notValidEmail.classList.remove("hidden");
    notValidPassword.classList.remove("hidden");
    emailInput.classList.add("error");
    passwordInput.classList.add("error");
  }
}

function validationByEnter(e) {
  if (e.key === "Enter") {
    validation();
  }
}

signinBox.addEventListener("focusin", reset);
signinBox.addEventListener("focusout", inputCheck);
signinBox.addEventListener("keydown", validationByEnter);
signinBtn.addEventListener("click", validation);

변경 후 JS

const signunBox = document.querySelector(".signun-box");
const signinBtn = document.querySelector(".signun-btn");

const emailInput = document.querySelector(".sign-input-email");
const passwordInput = document.querySelector(".sign-input-password");
const passwordCheckInput = document.querySelector(".sign.-input-pa");

const emailErrorMsg = document.querySelector(".email-error-msg");
const passwordErrorMsg = document.querySelector(".password-error-msg");

const TEST_EMAIL = "test@codeit.com";
const TEST_PW = "codeit101";

const EMPTY_EMAIL_MSG = "이메일을 입력해주세요.";
const INVALID_EMAIL_MSG = "올바른 이메일 주소가 아닙니다.";
const LOGIN_FAIL_MESSAGE_EMAIL = "이메일을 확인해주세요.";

const EMPTY_PASSWORD_MSG = "비밀번호를 입력해주세요.";
const LOGIN_FAIL_MESSAGE_PASSWORD = "비밀번호를 확인해주세요.";

function validateInput(inputEl, errorMsgEl, message) {
  errorMsgEl.textContent = message;
  if (message) {
    inputEl.classList.add("error");
    return;
  }
  inputEl.classList.remove("error");
  return;
}

function validateEmail() {
  const email_regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/i;

  if (emailInput.value === "") {
    return validateInput(emailInput, emailErrorMsg, EMPTY_EMAIL_MSG);
  }
  if (!email_regex.test(emailInput.value)) {
    return validateInput(emailInput, emailErrorMsg, INVALID_EMAIL_MSG);
  }
  return validateInput(emailInput, emailErrorMsg, "");
}

function validatePassword() {
  if (passwordInput.value === "") {
    return validateInput(passwordInput, passwordErrorMsg, EMPTY_PASSWORD_MSG);
  }
  return validateInput(passwordInput, passwordErrorMsg, "");
}

function validatePasswordCheck() {
  if (passwordCheckInput.value === "") {
    return validateInput(passwordInput, passwordErrorMsg, EMPTY_PASSWORD_MSG);
  }
  return validateInput(passwordInput, passwordErrorMsg, "");
}

function signin() {
  if (emailInput.value == TEST_EMAIL && passwordInput.value == TEST_PW) {
    window.location.href = "/forder.html";
  } else {
    validateInput(emailInput, emailErrorMsg, LOGIN_FAIL_MESSAGE_EMAIL);
    validateInput(passwordInput, passwordErrorMsg, LOGIN_FAIL_MESSAGE_PASSWORD);
  }
}

function validationByEnter(e) {
  if (e.key === "Enter") {
    e.preventDefault();
    signin();
  }
}

emailInput.addEventListener("focusout", validateEmail);
passwordInput.addEventListener("focusout", validatePassword);
signinBtn.addEventListener("click", signin);
signunBox.addEventListener("keydown", validationByEnter);

문제발생

리액트 연습용으로 깃허브 리포 하나로 여러 연습용 프로젝트를 관리하고 싶었다.

그래서 이미 연습용으로 파둔 리포에다가 다른 프로젝트 두개를 넣고 푸시를 해봤다.

사실 이렇게 하면 어떻게 되나 궁금하기도 했고 까짓거 여차하면 리포 새로 파지~ 하는 생각으로 저질렀다.

하지만 다른 컴퓨터로 리포 클론해서 확인 결과 빈 폴더가 나오고 멀티 모듈 설정하라는 소리가 나오길래 빠른 포기.

 

문제해결

깃을 공부하기 이전의 나라면 포기하고 불편한 빈 폴더 두개를 남겨둔채로 쓰던가, 새로운 리포지토리를 팠겠지만

깃의 동작원리와 커맨드를 공부한 나는 다르다!

마침 얼마전 공부했던 git revert 취소할커밋id 를 이용해서 원상복구 시켰다.

 

 

사실 revert 뒤에 커밋id 를 되돌릴 시점으로 해야하는지 취소할 커밋으로 해야하는지 헷갈려서 한번 잘못 쓴

사소하고 앙증맞은 찐빠가 있었지만 그 또한 어떠하랴.

+ Recent posts