< 목차 >
1. Warning: Each child in a list should have a unique "key" prop. ➡️ 부모 컴포넌트의 map 돌리는 곳 최상위 태그에서 key prop 전달해 해결
2. [다크모드 Error] Warning: Extra attributes from the server: class,style ➡️ 현재 마운트 상태를 확인하는 State 추가해 해결
3. Encountered two children with the same key, `faded`. ➡️ `faded` key 속성을 제거해 해결
4. commentsData 객체를 Comment[] 타입으로 강제 변환하려 발생한 Error ➡️ runtime에 데이터를 검증하는 코드 추가해 해결
5. GET https://njltrjqccgmfpfdlyldi.supabase.co/rest/v1/study_place_scraps?select=id&user_id=eq.ef676003-fbc4-452c-871d-53580397ded5&study_place_id=eq.undefined 400 (Bad Request)
6. 메인 페이지에서 로그아웃 후에도 Navbar의 상태가 업데이트되지 않음 ➡️ Console.log로 data 직접 확인해 해결
7. 금일 소감
1. Warning: Each child in a list should have a unique "key" prop. ➡️ 부모 컴포넌트의 map 돌리는 곳 최상위 태그에서 key prop 전달해 해결
왼쪽은 에러 메시지, 오른쪽은 에러가 발생한 부분의 코드다.
위 사진 속 코드에서 key prop을 자식 컴포넌트에서 직접적으로 추가하고 있는데, 저렇게 쓰면 안 된다.
key prop은 React에서 리스트의 각 항목을 식별하는 데 사용된다. 그래서 CustomMainCard 컴포넌트 자체에 key prop을 추가하는 것은 의미가 없다. 대신 Main 컴포넌트에서 CustomMainCard 컴포넌트를 렌더링할 때, 각각의 CustomMainCard 컴포넌트에 key prop을 제공해야 한다.
따라서 Main 컴포넌트에서 CustomMainCard를 렌더링할 때의 key prop을 제거하고, Main 컴포넌트에서 CustomMainCard 컴포넌트를 렌더링할 때 각각의 CustomMainCard 컴포넌트에 key prop을 제공해야 한다.
위와 같이 코드를 수정했더니 해결되었다! key prop을 자식 컴포넌트(CustomMainCard)에선 제거하고 부모 컴포넌트에만 부여했다. 부모 컴포넌트에서는 div 요소가 아닌 map을 돌리는 최상위 태그인 React.Fragment에 key prop을 주었다.
2. [다크모드 Error] Warning: Extra attributes from the server: class,style ➡️ 현재 마운트 상태를 확인하는 State 추가해 해결
이게 무슨 말인가 싶어서 일단 에러 메시지를 구글링 했더니 정말 쉽게 해결방안을 찾았다. 웬만한 건 혼자 알아서 해결책을 찾아내지만 쉽게 해결할 수 있으면 굳이 돌아갈 필요가 없는 법,, 아무튼 위 왼쪽에서 3번째 사진의 코드를 복사 및 붙여넣기 하면 될 듯하여 설명 좀 읽고 바로 적용해보았다.
위와 같이 코드를 수정하니 바로 해결이 되었다.
원인은 Server에서 첫 렌더링 시, useTheme의 theme는 서버에서 알 수 없는 상태이기 때문에 undefined 상태이고, ThemeProvider가 렌더링 되면 theme의 상태가 존재하므로 server와 client의 구조가 달라지게 되어 hydration 에러가 발생하는 것이다.
따라서 Providers 컴포넌트에 현재 마운트 상태를 확인하는 상태 isMount를 false로 초기화한 뒤 isMount가 false일 때는 null을 return 하고, true일 때는 ThemeProvider를 리턴하도록 작성하면 해당 에러가 사라지며 해결된다.
[Reference]
3. Encountered two children with the same key, `faded`. ➡️ `faded` key 속성을 제거해 해결
Warning: Encountered two children with the same key, `faded`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
위 Error Message는 React가 렌더링을 최적화하기 위해 사용하는 고유 key가 배열이나 반복되는 요소에 중복으로 사용되었다는 의미다. 같은 key 값을 가진 컴포넌트들은 React에 의해 식별이 어려워지고, 이는 렌더링 중인 목록이 업데이트될 때 예기치 않은 문제를 일으킬 수 있다.
자 이제 오랜 시간 나의 속을 썩인 잔망둥이 범인을 보러 가보자 ^^*
문제가 되었던 코드
<Textarea
name='title'
key='faded' // 이 key가 문제를 일으킴
variant='faded'
label='Title'
labelPlacement='outside'
value={comment.title}
onChange={handleCommentInputChange}
placeholder='Enter the title'
className='col-span-12 md:col-span-6 mb-6 md:mb-0 font-bold'
/>
<Textarea
name='contents'
key='faded' // 동일한 key가 여기에도 사용됨
variant='faded'
label='Contents'
labelPlacement='outside'
value={comment.contents}
onChange={handleCommentInputChange}
placeholder='Enter the contents'
className='col-span-12 md:col-span-6 mb-6 md:mb-0 font-bold'
/>
key는 반복적으로 렌더링되는 컴포넌트 목록에서 각 컴포넌트를 고유하게 식별하는 데 사용되어야 한다. 이 경우, 두 <Textarea /> 컴포넌트 모두 같은 key 값을 가지고 있기 때문에 경고가 발생한다.
이 문제를 해결하기 위해, 각 컴포넌트에 고유한 key 값을 제공해야 한다. 굳이 여기서 key 속성이 필요하지 않다면, 아예 제거할 수도 있다. key는 list에서 아이템들이 추가, 제거, 정렬될 때 React가 아이템들을 추적하는 데 사용된다. 지금의 코드는 이러한 상황에 해당하지 않으니 당당하게 삭제할 수 있다.
따라서.. 해결책은 단순히 key 속성을 제거하면 된다 ^^... 그럼 말끔히 해결된다. 기능도 여전히 잘 작동한다.
key='faded'를 사용한 이유는 Next-UI에서 제공하는 Textarea 속성 코드에 있었기 때문이다. 이번 경험을 통해, 앞으로는 아무리 CSS 관련 코드더라도 꼼꼼히 보고, 불필요하거나 문제될 것은 없는지 정확하게 확인하고 넘어가는 습관을 형성해야겠다는 다짐을 굳게 하였다. 휴...
4. commentsData 객체를 Comment[] 타입으로 강제 변환하려 발생한 Error ➡️ runtime에 데이터를 검증하는 코드 추가해 해결
Comment Type과 관련하여 이렇게 기나긴 Type Error가 뜬다.
이 Error는 commentsData 객체를 Comment[] 타입으로 강제 변환하려 할 때 발생한다. 이 문제는 commentsData가 실제로 Comment[] 타입의 구조와 일치하지 않을 때 발생할 수 있다.
아닐 것 같긴 하지만 '뭐라도 시도해보자' 라는 생각에 지정한 이름을 삭제하고 원본 그 자체인 'data'로 해보기도 했지만 역시나 해결이 되지 않았다. 어떻게 할까 하며 이래저래 시도해보며 이 Type Error에 많은 시간을 할애했다. 브라우저 console창에서의 Error도 싫지만 특히 VS Code에 Error가 뜨는 건 못참지..
해결책은 데이터가 올바르게 구조화되었다고 가정하고 강제로 변환하는 것이 아니라, '런타임에 데이터를 검증하는 방법'이다!
런타임에 commentsData가 배열인지 확인하고, 배열의 각 요소를 Comment 타입에 맞게 매핑한다. 이 방법은 타입 오류가 나지 않도록 하면서도 데이터 구조에 유연성을 부여한다.
변경된 코드는 다음과 같은 처리 과정을 거친다. 좀 더 자세하게 알아보자.
- if (Array.isArray(commentsData)) { ... }
: 이 조건문은 commentsData가 배열인지 확인한다. 데이터베이스로부터 받은 결과가 배열 형태일 때만 내부 로직을 실행하도록 하여, 배열이 아닐 경우 (예: null, undefined 또는 오브젝트 등) 잘못된 접근으로 인한 오류를 방지한다. - commentsData.map((comment: any) => { ... })
: 배열이 확인되면, .map() 함수를 사용하여 배열의 각 항목을 순회하며, 새로운 형태의 객체로 변환한다. 여기서 comment: any는 TypeScript에게 해당 항목의 타입을 지정하지 않음을 의미한다. 이는 comment 객체의 구조가 미리 정해진 것이 아니거나, 복잡한 구조를 가지고 있을 때 유용할 수 있다. - [객체 변환 로직] return { ...comment, study_place: comment.study_place };
: 각 댓글 객체에 대해, 전개 연산자(...)를 사용하여 기존의 comment 객체에서 모든 프로퍼티를 새 객체로 복사한다. 그리고 study_place 프로퍼티를 명시적으로 comment.study_place로 설정한다. 이는 study_place 프로퍼티가 선택적(optional)으로 선언되었거나, 특별한 처리가 필요할 때 유용하다.
여기서 study_place 프로퍼티는 데이터베이스에서 가져온 study_places 테이블의 place_id와 place_name을 포함하는 객체다. comment.study_place는 해당 댓글이 어떤 공부 장소에 대한 것인지를 연결해주는 정보를 담고 있다. - setUserComments(typedComments)
: 최종적으로 변환된 댓글 객체 배열 typedComments를 setUserComments를 통해 상태로 설정한다. 이는 React 컴포넌트의 상태를 업데이트하여, 변환된 댓글 데이터를 애플리케이션의 다른 부분에서 사용할 수 있도록 한다.
코드의 이 부분은 데이터베이스로부터 가져온 원시 데이터를 애플리케이션에서 사용하기 적합한 형태로 변환하고, 이를 컴포넌트의 상태에 저장하여 React 컴포넌트가 렌더링하는 데 사용하도록 하는 처리 과정이다. Comment 인터페이스를 통해 TypeScript가 이 데이터의 구조를 이해할 수 있도록 타입 안정성을 부여하는 것이 중요한 역할을 한다.
5. GET https://njltrjqccgmfpfdlyldi.supabase.co/rest/v1/study_place_scraps?select=id&user_id=eq.ef676003-fbc4-452c-871d-53580397ded5&study_place_id=eq.undefined 400 (Bad Request)
위 사진 속 에러는 "400 (Bad Request)"는 서버가 클라이언트의 요청을 처리할 수 없다는 것을 의미한다. URL에서 study_place_id=eq.undefined를 볼 때, 문제는 studyPlace.id가 undefined로 평가되어 API 요청에 undefined가 문자열로 전송되었다는 것을 알 수 있다. 이는 studyPlace 객체가 null이거나 id 속성이 정의되지 않았을 때 발생할 수 있다.
checkScrapStatus 함수에서 문제가 발생했을 가능성이 높으니 한 번 봐보자. 이 함수는 studyPlace 객체의 id 값을 사용하여 Supabase 쿼리를 만든다. 만약 studyPlace 객체가 아직 설정되지 않았거나, id 필드가 없다면, 쿼리는 잘못된 값을 사용하여 만들어진다.
문제를 해결하기 위해서는 studyPlace 객체와 그 id 필드가 유효한지 확인해야 한다. 위 코드는 checkScrapStatus 함수 내에서 이를 확인하는 방법이다. session?.user?.id와 studyPlace?.id가 둘 다 존재하는지 확인한다. 옵셔널 체이닝(?.)은 해당 값이 undefined 또는 null이 아닐 때만 접근하도록 한다. 만약 값이 undefined 또는 null이면, 나머지 코드는 실행되지 않는다.
또한, 컴포넌트가 처음 렌더링될 때 studyPlace가 설정되지 않았을 수 있으므로, useEffect 훅에서 studyPlace가 설정된 후에 checkScrapStatus를 호출하도록 해야 할 수도 있다. 위와 같이 하면 studyPlace가 설정되기 전에는 checkScrapStatus 함수가 호출되지 않아, 요청 URL에 undefined가 포함되는 문제를 방지할 수 있다. 해결 완료!
6. 메인 페이지에서 로그아웃 후에도 Navbar의 상태가 업데이트되지 않음 ➡️ Console.log로 data 직접 확인해 해결
메인 페이지에서 로그아웃한 후에도 Navbar의 상태가 업데이트되지 않았다. 위 코드가 바로 그때 당시 코드다.
로그아웃 후에도 Navbar의 상태가 업데이트되지 않는 문제는 Supabase 세션이 변경될 때 React 상태가 업데이트되지 않기 때문에 발생할 수 있다. 이를 해결하기 위해서 Supabase 세션의 변경을 감지하고 그에 따라 React 상태를 업데이트해야 하기 때문에 공식 문서를 찾으러 갔다.
Supabase는 세션 변경을 감지할 수 있는 onAuthStateChange 리스너를 제공한다. 이 리스너를 사용하여 세션의 변경을 감지하고, 세션이 로그아웃 상태로 변경되면 상태를 업데이트할 수 있다.
그래서 onAuthStateChange를 이용하여 세션 상태를 업데이트하는 방법을 나타낸 코드 예제를 보고 따라 했는데?..
위와 같은 에러가 떴다. 에러 메시지를 전체적으로 보면 다음과 같다.
왜?... 공식 문서에서 하란 대로 했는데 왜 없다는 거지?... 이것 때문에 코드를 이래저래 바꿔도 보며 시도를 많이 했다. 끝내 내가 내린 결론은 '공식 문서대로 해보지 말고 확인을 한 번 해보자'였다. 그래서 console로 직접 data를 확인해보니
하.... ^^ Supabase.. 일 안 하시는 겁니까?,, 아니 data.subscription.unsubscribe 잖아요.. 왜 잘못 알려주시는 건데요 ㅠㅠ... 차라리 subscription을
React.useEffect(() => {
const { subscription } = supabase.auth.onAuthStateChange(
(event, session) => {
if (event === 'SIGNED_OUT') {
setSession(null)
} else if (session) {
setSession(session)
}
})
return () => {
subscription.unsubscribe()
}
}, [])
이렇게 구조분해할당으로 알려줬으면 그나마 차라리 나았.. 아니다 그냥 똑같다. 왜... 제대로 안 적어둔 거야... 하... 이것 때문에 꽤 시간 썼다..
아무튼 해결한 코드는 위와 같다. 코드 자체를 설명하자면.. onAuthStateChange 리스너를 설정한 후, 로그아웃이 발생하면 이 리스너가 트리거되고, setSession(null)을 호출하여 세션 상태를 null로 설정한다. 이로 인해 token 상태가 변경되고, Navbar 컴포넌트가 새로운 상태를 반영하여 리렌더링된다.
onAuthStateChange 리스너는 인증 상태(로그인, 로그아웃, 회원가입 등)가 변경될 때마다 호출되며, 리스너를 설정할 때 반환받은 authListener를 사용하여 컴포넌트가 언마운트될 때 이 리스너를 제거하여 메모리 누수를 방지할 수 있다.
이렇게 하니 메인 페이지에서 로그아웃해도 Navbar의 상태가 정상적으로 업데이트되었다.
7. 금일 소감
정말... 믿었던 공식 문서까지 저렇게 대충..도 아니고 잘못 알려주다니... ㅠㅡㅠ 휴 역시 언제나 확인은 필수인 듯하다. 이틀 간 트러블 슈팅에 전념해버렸네,, 에러가 많긴 했는데 전부 다 잡고, 기능에 뭔가 어색한 부분과 불편한 부분을 전부 수정하고 개선하려고 하니 이것도 생각보다 시간이 좀 더 드는 것 같다.
기능이 쾌적하게 스무스하게 슉슉 처리되고 처리 속도가 빠르려면 많은 노력이 들어간다는 것을 다시 한번 깨닫게 되었다. 이런 과정에서 어려움도 꽤 있긴 하지만 무척 재밌어서 좋다.
개인 프로젝트를 처음 하다 보니 느낀 점이 있다. 팀 프로젝트는 협업이다 보니 같이 하는 즐거움이 있고 서로 돕고 도우며 느끼는 희열이 있는 반면, 개인 프로젝트는 그런 감정과 과정이 없다. 하지만 확실히 모든 걸 혼자 다 해야 하는 부분이 있기 때문에 자립심과 구현력이 더 빨리 느는 것 같다. Error도 전에는 웬만큼 하다가 튜터님들께 찾아가거나 동기들에게 질문했는데 이젠 많은 부분을 내가 해결하고 또 최대한 내가 해결하려고 한다. 그리고 전부 내 손길이 들어간 코드다 보니 애착도 많이 간다.
팀/개인 프로젝트 모두 장단점이 확실해서 좋다. 다음은 신뢰 있고 함께 하며 서로 돕고 배우며 성장할 수 있는 팀원들과 팀 프로젝트를 꼭 해보고 싶다. 암튼 이번 주도 쭉쭉 진도 나가자 파이팅!!
'[Front-end] 개발자 공부' 카테고리의 다른 글
[개발 공부 66일차] 끝은 새로운 시작! | User Test 전, SPL (0) | 2024.05.02 |
---|---|
[개발 공부 65일차] 트러블 슈팅, 우수 TIL 선정 (3) | 2024.04.21 |
[개발 공부 63일차] 트러블 슈팅, Hydration Error (0) | 2024.04.09 |
[개발 공부 62일차] 개인 프로젝트 진행 상황 | 트러블 슈팅, 매핑 (2) | 2024.04.05 |
[개발 공부 61일차] 그간의 상황 및 앞으로의 계획 | 트러블슈팅, MVP, CI/CD (1) | 2024.03.31 |