뒤로
자허토르테
자허토르테 ·

2024년 개인 프로젝트 제작기 (2)

🗣️ 이번의 이야기

저번에는 프로젝트의 계기와 그 준비 과정에 대한 이야기를 적었다.

photo-1522435229388-6f7a422cd95b.jpg

여기서는 1주차에서 기본 요소라고 생각한 페이지들을 구현하면서 배우거나 알게 된 점들을 적어보고자 한다.

Next.JS가 아닌 Vite로 진행하니까 구현할 때, 설정 면에서 차이점을 느끼기도 했고, 또 하다 보니까 Firebase나 styled-components 등 이미 익숙하다고 생각했던 것에도 고민하거나 헤멘 부분들이 나오기도 했다.

그 중에는 진짜 글자 하나 때문에 발생한 사소한 실수도 있었는데, 그런 것까지도 이번에는 잊지 말자고 생각해 이렇게 글로 남겨봤다.

⚠️ Vite에서 환경 변수를 불러올 때의 주의점

Next.JS 등을 사용할 당시에 env 파일에 배치한 환경 변수를 불러올 때, 보통 process.env.(변수명)으로 불러왔던 기억이 있다.

Vite에서 환경 변수를 불러올 때는 그 형식이 좀 다르다는 걸 깨달았다.

  1. 일단 env 파일에서는 변수명 앞에 VITE_라는 접두사를 붙이자.

Untitled (19).png

Vite 내에서 사용할 환경 변수를 정의하고 싶다면, ‘VITE_’라는 접두사를 붙여야 한다. (envPrifix를 통해서 접두사 규칙을 변경할 수 있는데, 그게 아니라면 저 접두사를 꼭 붙여주자.)

  1. 환경 변수를 받아올 때에는 process.env.(변수명)이 아닌 import.meta.env.(변수명)이다.

Untitled (18).png

Vite에서 환경 변수를 쓸 때는 위의 이미지처럼import.meta.env.(변수명)으로 불러와 써야한다.

왜 이렇게 써야하는지, 변경할 수는 없는지 궁금해서 찾아봤지만 별다른 언급을 찾지 못해서 이대로 써야하는 것이 규칙인 듯 하니 Vite로 프로젝트를 하는 사람들은 이 점을 주의하는 게 좋겠다.

📩 Vite에서 public 폴더 경로에 있는 파일을 불러오는 방법

프로젝트 내 이미지 파일이나 svg 등 에셋들은 전부 public 폴더에서 관리하고 있다.

Vite에서는 참 고맙게도 public 폴더 자체에 대한 접근 경로를 따로 설정할 필요 없이 public 디렉토리를 바로 바라보도록 적용되어있다.

위와 같이 public 디렉토리가 기본적인 설정으로 되어있어서, 해당 디렉토리 내 추가 경로만 작성해주면 원하는 에셋을 가져올 수 있다.

🤦 env 파일에 배치한 Firebase API 변수 값을 받아오지 못 하는 문제

결과부터 말하자면, env 파일 내 환경 변수를 불러올 때 글자 하나를 지우지 않아서 발생한 문제였다.

정말 단순한 문제이지만, 오답 노트처럼 또 실수하지 않기 위해 남긴다.

Untitled (17).png

환경 변수에서 value 값 뒤에는 객체를 만들 때처럼 다음 key와 분리하기 위해 ‘,’를 찍을 필요가 없다.

바로 다음 줄로 넘어가서 다음 key와 value를 입력하면 되는데, 습관적으로 ‘,’를 찍은 바람에 나중에 API 테스트를 할 때 콘솔창에 Firebase의 API key가 없다는 경고가 나왔다.🤦

Untitled (16).png

사소한 문제지만, 그래도 안 짚고 가는 것보다 짚고 가는 게 중요하니 주의해두자.

❓ TypeScript에서 **try-catch** 문의 **error** 인수의 타입이 unknown이라고?

프로젝트 내에서 Firebase의 모듈을 데이터 처리를 할 때는 try-catch 문을 사용해여 에러 관리를 하고 있다.

근데 catch 문 안에서 error 메시지를 처리하려고 하니, error의 타입이 unknown으로 찍히고 있다는 것을 깨달았다.

Untitled (15).png

그냥 string으로 변환하면 되겠지-라고 생각하다가, 돌이켜보니 어라? 하고 급 호기심이 생겼다.

  • 왜 unknown인 걸까?

  • 그럼 해당 타입을 고려해서 예외 처리를 어떻게 진행하면 좋을까?

우선 왜 unknown인가에 대한 답을 찾기 위해서 구글링을 해봤는데, unknown인 타입 상태에서 어떻게 하면 문제를 해결할 수 있는가와 연관된 글은 많고, 왜 unknown으로 적용된 것인지에 대한 글은 찾기 힘들었다.

결국, 이 경우엔 ChatGPT의 힘을 빌려서 알아봤는데..

Untitled (14).png

결과적으로 catch 문 속에서 어떤 타입의 오류가 발생할지 모르므로 안정성 및 예측 가능성을 고려해 해당 타입이 디폴트로 적용된 것이라는 내용이었다.

단, 타입이 이런 만큼 error의 값을 제대로 사용하려면 타입을 명확하게 처리해야 한다. 안 그러면 아래와 같은 이슈가 발생한다.

Untitled (13).png

자, 이제 error의 타입이 왜 unknown인지 알았으니, 예외 처리를 진행해보고자 한다.

‘왜?’ 라는 글은 찾기 힘들었어도, ‘어떻게?’ 라는 글은 많아서 둘러봤는데 대다수의 글이 ‘타입 좁히기/내로잉(Narrowing)’을 이용해 문제를 해결했다.

Object is of type 'unknown'

위의 글에서 catch 문의 error 인자값을 처리하는 방법으로 instanceof를 활용했다.

instanceof란 대상이 ****어떤 class나 생성자 함수를 사용하여 생성됐는지를 판단해주는 연산자이다.

(한동안 잊고 있었다가 예전에 이고잉 님의 강의를 보면서 정리했던 내용이 있는데, 궁금한 사람은 해당 내용을 참고해주길 바란다.)

이 연산자를 이용해서, error 인자에서 받아오는 값이 Error class를 가지고 있는지 분간하여 예외 처리를 진행하면 된다.

Untitled (12).png

기본적으로 흔히 우리가 자주 보는 Uncaught Error와 같이 콘솔에서 찍히는 에러들은 Error class를 가지고 있다.

따라서 이에 해당하는 친구들을 if문을 통해 분류하고, 아닌 친구들은 우선 문자열로 변환해 반환되게 임시로 처리했다.

지금 당장은 이 정도면 충분할 것 같다.

추후 프로젝트가 어느 정도 진행되면 Sentry를 배치할 예정인데, 양식을 만들고 나서 수정해도 늦지는 않을 거다.

🤔 Firebase Auth 템플릿의 작업 URL 처리, 그리고 페이지 렌더 이슈…

이렇게만 보면 어떤 문제인가 싶은데, 우선 Firebase Auth의 이메일 템플릿을 적용할 수 있는 부분은 이번 프로젝트에서 크게 두 가지로 갈린다.

  • 이메일 주소 인증을 위한 이메일 (본인 인증)

  • 가입한 이메일 주소를 통해 비밀번호 재설정 링크를 받을 이메일 (비밀번호 변경)

두 이메일은 같은 작업 URL을 공유하게 되는데, 받은 이메일을 통해 들어가야 하는 path가 각기 다른 점 때문에 작업 URL을 분리할 수 없다는 단점이 발생했다.

  • 이메일 주소 인증: /signupcomplete

  • 비밀번호 재설정: /findpassword

만약 이메일 주소 인증 경로(‘~~/signupcomplete’)로 작업 URL을 설정했을 시, 위의 두 케이스에서 보내주는 템플릿 메시지 내 URL은 아래와 같이 적용된다.

Untitled (11).png

이 경우, 비밀번호 재설정도 이메일 주소 인증 페이지로 이동하게 된다는 단점이 발생한다.

결국 어떤 식으로 동일한 작업 URL을 유지하게 할 것이냐가 문제 해결의 포인트인데, 이전 프로젝트에서는 하나의 페이지에서 switch-case를 이용해 경우에 따라 다른 컴포넌트들을 받아오게 했다.

하지만 이제와서 보니 case에 엮일 컴포넌트들을 다 배치해야 하는 만큼 무겁지 않을까? 비효율적이지 않을까? 라는 의문이 들어 좋은 방법이 아니라고 생각했다.

그렇다면 이번엔 차라리 공용으로 쓸 새로운 path를 추가하고 Custom Hook을 이용해 케이스에 따라 다른 path로 redirect 해주는 건 어떨까? 라고 생각해 쌈마이하게 방법을 바꿨다.

Untitled (10).png

redirect라는 path를 추가한 다음, Custom Hook에서는 진입한 링크의 params 중에 mode의 값이 이메일 인증이냐, 비밀번호 재설정이냐에 따라 react-router-dom의 useNavigate를 통해서 페이지를 redirect하도록 유도했다.

Untitled (9).png

보통이라면 그냥 useNavigate를 활용해서 보내주면 되겠지? 라고 생각하고 경로를 추가해서 바로 보내줬는데… 페이지로 이동은 되지만 렌더가… 바로 되지 않았다.

ezgif-2-824514c18a.gif

콘솔을 확인해보니 에러로 나오는 사항도 없었기 때문에 왜 이런 일이 생겼는지 솔직히 아직도 원인을 모르겠다.. 누군가 알려줬으면!

어쨌든 문제는 해결해야 하니까, ‘어떻게 하면 될까?’라고 고민하던 중에 혹시 렌더 타이밍이 너무 빠른 걸까? 싶어서 아래와 같이 setTimeout을 배치하고 시도해봤다.

Untitled (8).png

그래서 테스트 결과는 아래와 같이 문제 없이 redirect되면서, 페이지 렌더링도 잘 적용됐다.

ezgif-5-20eb812dab.gif

정말로 렌더 타이밍이 너무나도 빨라서 그런 걸까?

문제가 해결됐으니 만족스럽긴 한데, 정확한 원인을 모르니 체기가 남은 느낌이 든다...

😖 useLayoutEffect의 Dependency Array가 비어있는 데도 두 번 돈다?

이메일 인증 완료 처리와 관련하여 위의 이슈와는 별개로 인증 완료 페이지에 처음 진입 시, 의존성 배열(dependency array)를 비워놔도 useLayoutEffect 속 로직이 한 번 더 동작하는 것을 깨달았다.

그러다보니 첫 동작 시에 인증은 완료되지만, 렌더 후에 한 번 더 인증 절차가 작동하게 되는 이슈가 발생했다.

다행히 Firebase 쪽에서는 비활성화된 코드라면서 에러를 내뱉고 동작을 막아줘서 큰 문제가 되지는 않는데, 그럼에도 한 번만 동작하면 되는 것을 두 번 동작되고 그러니 의도한 그림과는 달라서 이 부분을 해결해보고 싶었다.

Untitled (7).png

예전에 useEffect와 useLayoutEffect를 공부했을 때, 둘의 차이점은 렌더링 전/후를 기준으로 첫 동작 시점에 차이가 있다는 것 뿐이었고 의존성 배열에 대해선, 두 훅 모두 비어있다면 첫 동작만 실행된다는 공통점이 있다는 걸 기억하고 있다.

혹시 내가 잘못 알고 있었나..? 싶어서 일단 useLayoutEffect 훅 내 코드에 문제가 없던 건 아닌지 고쳐보기로 했다.

우선 인증 처리 함수를 useLayoutEffect 외부에서 생성해, 해당 함수를 useLayoutEffect 안에서 호출하도록 했다.

const applyActionCodeAndVerifiedAccount = useCallback(async () => {
  const actionCode = await searchParams.get("actionCode");
  if (actionCode === null) {
    throw new Error("The Action Code is invalid.");
  }
  await applyActionCode(auth, actionCode);
},[auth, searchParams]);

useLayoutEffect(() => {
  applyActionCodeAndVerifiedAccount();
}, [applyActionCodeAndVerifiedAccount]);

이 때, 외부로 뺀 함수 applyActionCodeAndVerifiedAccount는 렌더링이 될 때마다 유지되는 게 아니라 새로 생성되는데 그러면 useLayoutEffect도 해당 함수가 변화한 것을 인지하고 리렌더링을 일으키므로 잘못하다간 무한 리렌더링을 유발할 수 있다.

그래서, 함수에 useCallback을 적용하여 더 이상 렌더링이 일어나지 않게 방지했다.

(아, 참고로 저렇게 처리를 안 하면 React가 The ‘~~~’ function makes the dependencies of useLayoutEffect Hook change on every render.라면서 친절하게 수정하라고 경고해준다.^^)

저렇게 함으로서 함수도 변경되는 사항이 없을테니 한 번만 돌 것이라 생각하고 테스트해봤는데.. 똑같이 한 번 더 돌아서 에러가 내뱉어지는 건 여전했다.

Untitled (6).png

이러다보니 아예 함수를 useLayoutEffect 안에 넣어, 훅 내부에서만 돌도록 하면 될까? 라고 생각했지만, 이거도 생각해보니까 useLayoutEffect 속 내용이 두 번 도는 건 변함이 없다.

사고가 마비된 느낌이라서, 잠시 쉬었다가 다시 곰곰히 생각해봤는데..

그럼 사실 useLayoutEffect는 문제가 없고 외부의 요인에서 문제가 있는 건가? 🤔

접근을 다르게 보고 검색을 시도해봤더니 아래와 같은 블로그 글이 보였다.

[개발일지 #3] UseEffect는 왜 두번 실행되는걸까?

한 마디로, React에서 내부 로직을 엄격하게 검사하기 위한 StrictMode의 영향이라는 듯 하다. 그래서 StrictMode를 지워보고 테스트해봤다.

Untitled (5).png

깔끔하게 해결이 됐다.. 휴, Hook과 관련하여 내가 여태까지 잘못 알고 있었나 싶어서 걱정했는데 다행히 틀리진 않은 것 같다.

아무튼 원인을 알았으니 StrictMode를 지우고.. 쓰려고 했는데 내부 코드까지 검사해준다는데 개발 당시에는 지우지 않아도 괜찮을 것 같아서 유지하기로 했다.

🔍 닉네임 글자 검사를 Byte 수로 체크하면 안되는 걸까?

이번 프로젝트는 다국어 지원을 염두해두고 있는데, 그러다보니 닉네임과 관련하여 고민이 생겼다.

‘사람마다 닉네임을 지을 언어가 다 다를텐데 이걸 어떻게 처리하는 게 좋을까’라는 부분이었는데 이에 대한 해결책으로는 Byte로 계산해서 처리하자! 라고 답이 쉽게 나왔다.

흔히 기억하기로 영어랑 숫자는 1바이트이고, 한글이나 일본어, 한자는 2바이트라는 이야기를 건너건너 들은 적이 있어서 그렇게 처리하면 되겠지? 라고 생각했는데…

Untitled (4).png

…정말 안일한 생각이었다는 걸 반성한다. 뭐든지 건너 들었다고 해서 ‘이거면 되겠지?’라고 생각하지 말고, 검증을 해보자.

  1. 우선 string을 Byte로 계산하고 싶은데 방법이 있을까?

있다. Blob 인스턴스를 통해 string을 UTF-8로 인코딩해서 Byte 수를 계산하면 된다.

Get Byte size of the string in Javascript

function countStringConvertToBytes(string) {
    return new Blob([string]).size;
}

이렇게 하면 string이 총 몇 Byte인지를 알 수 있다.

  1. 그래서 영어와 숫자는 1 Byte, 일본어랑 한국어는 정말 2Byte일까?

결과는 아래 이미지로 대체하겠다.

Untitled (3).png

‘16 Byte로 제한하면 되겠지’라고 단순하게 접근한 내 스스로가 원망스럽다..

이렇게 될 경우, 한글과 일본어는 의도와 다르게 5글자까지 입력이 될 것이고, 영어와 숫자는 혼합하더라도 16자까지 입력은 가능하다.

여기서 더 케이스를 생각해본다면..

  • 차라리 24 Byte로 늘리는 건?

    • 영문, 숫자도 24글자 입력되는 건 너무 긴 것 같다.

  • 그렇다면 영어랑 숫자만 있다면 16Byte로 제한하는 건?

    • 사이에 한글이나 일본어가 들어간 순간의 예외 처리는?

      • x지죤너구리x 같은 닉네임의 처리는?

Untitled (2).png

이러다보니 답이 도저히 나오지 않았고, 끝내 대안을 두 가지 정리해봤다.

  • 영어와 숫자로만 닉네임을 만들 수 있도록 하여 16자까지 입력 가능하도록.

  • 혹은 이메일 주소의 아이디 부분을 닉네임으로 강제로 부여하도록.

그 밖에도 정규식으로 해결볼 수 있지 않을까? 라는 생각도 해봤는데, 이건 ChatGPT를 통해서 좀 핑퐁해봐야 할 것 같고… 닉네임이 쓰이는 페이지가 현재로서는 메인 페이지 밖에 없어서 우선은 2안으로 진행하기로 했다.

사실 1안이 가장 Best라고 생각하고 있는데, 우선은 기능 추가 전까지는 2안을 유지하고, 분명 닉네임을 더 쓸 수 있는 요소가 추가되면 1안으로 바로 되돌릴 수 있도록 유연하게 생각하기로 했다.

💅 styled-components의 prop 처리 - Transient Prop?

공용 버튼 컴포넌트를 제작한 후, 페이지에 배치를 하던 중에 bgColor라는 props을 만든 뒤, ‘돌아가기’ 버튼의 배경색을 변경해야 해서 해당 prop에 ‘invalid’로 값을 적용했던 적이 있었다.

그러더니 콘솔에서 다음과 같은 에러가 출력했다.

Untitled.png

해당 에러가 발생하는 이유는 bgColor라는 저 Prop이 DOM에 있는 HTML 요소들에 직접적으로 연결될 수 있다는 건데, 에러 메시지 쪽에서는 이런 문제를 방지하기 위해 스펠링을 모두 소문자 케이스로 변경하라고 하지만, props를 카멜 케이스로 쓰도록 규칙을 잡아놨으니 그건 좀 어려울 것 같았다.

다른 방법이 있을까 구글링을 해봤는데, prop의 앞에 달러 사인(’$’)을 붙여 해당 Prop이 HTML 요소에는 직접 관여하지 않으며, 오로지 styled-components에서만 쓰인다고 알리는, transient prop을 사용하라는 글들이 많았다.

Transient Props in styled-components

[ React ] styled-component에 props 보낼 때 나오는 warning 해결

Transient이 어떤 뜻인지 사전에서 알아보니 ‘일시적인, 순간적인, 일시적으로 머무르는’이라는 의미를 가지고 있다.

즉, ‘일시적인 프로퍼티’라는 건데 styled-components 문서를 보면 그 설명이 더 잘 나와있다.

스타일이 지정된 구성 요소에서 사용하도록 의도된 prop이 기본 React 노드로 전달되거나 DOM 요소로 렌더링되는 것을 방지하려면 prop 이름 앞에 달러 기호($)를 붙여 임시 prop으로 전환할 수 있습니다.

styled-components: API Reference

이걸 이용하면 styled-components에서만 사용하는 Prop을 DOM 요소에 직접 반영되는 문제를 피할 수 있다.

export type ButtonColorProp = {
  $bgColor?: "primary" | "invalid";
};
const Wrapper = styled.button<ButtonColorProp>`
  background-color: ${(props) =>
  isButtonBgColorPrimaryOrInvalid(props.$bgColor)};
`
export default function Button(props: ButtonProp) {
  const { text, type, bgColor, onClick } = props;
  return (
    <StButton.Wrapper type={type} $bgColor={bgColor} onClick={onClick}>
      {text}
    </StButton.Wrapper>
  );
}

이번 프로젝트에서 사용한 코드 중 일부인데, 버튼의 색을 경우에 따라 다르게 바꿔야 할 필요가 있다보니 위와 같이 작성하였다.

bgColor라는 Prop은 DOM 요소에는 없는 속성이니 이런 식으로 Transient Prop을 통해 styled-components에서만 반영되어서 쓰도록 해 DOM 요소에 영향을 주는 것을 방지했다.

한편, Transient Prop 관련 내용을 찾다보니 해당 기능을 사용할 때의 주의점을 정리해주신 분이 계셨는데 위의 기능을 쓰려는 사람들은 한 번 읽어보는 것이 좋을 것 같다.

Untitled (1).png

[styled-components] Transient props($propName) 사용 시 주의할 점 (feat. shouldForwardProp)

🎙️ 다음 이야기는…

1주차에서 하고 싶은 말을 다 했으니, 이제 2주차 내용을 정리하기 시작해야겠다.

2주차에는 메인 페이지와 준비물 생성에 관한 이야기를 작성하고자 한다. 슬슬 Firestore 배치도 진행하고, 준비물을 어떤 구조로 저장하고 받아올지도 생각해봐야 하고…

한 고비 산을 넘겼더니 다음 고비 산이 떡하니 나를 맞이해주고 있는데 그 산을 넘어가면 조금이라도 성장한 내가 있겠지? 긍정적으로 생각하고 작업에 집중해야겠다.

au revoir!

🔖 참고 자료
Vite-Vite의 환경 변수와 모드

Vite-envPrifix

Vite-public 디렉토리

[09/29] 'Uncaught ReferenceError: process is not defined' error

Object is of type 'unknown'

Get a catch block error message with TypeScript

TypeScript에서 catch block error message 사용하기

'instanceof'로 클래스 확인하기

A Complete Guide to useEffect — overreacted

React.js - exhaustive-deps-warning, react, react-hook

Get Byte size of the string in Javascript

Transient Props in styled-components

[ React ] styled-component에 props 보낼 때 나오는 warning 해결

[styled-components] Transient props($propName) 사용 시 주의할 점 (feat. shouldForwardProp)

인디 개발자 클럽 그룹의 글
1

댓글

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.