JSX element type does not contain syntax or call signatures.
토스트 ui를 작성하다가 만난 에러. 만들어둔 토스트 ui 컴포넌트를 아래와 같이 동기적으로 import해서 받아오려고 하니까 생긴 에러이다.
async function NoSsrEditor(): Promise<() => JSX.Element> {
const NoSsrEditor = await import('../components/ToastUI/TextEditor');
return NoSsrEditor.default;
}
const TextEditor = NoSsrEditor();
도무지 해결이 안 되서 전에 쓰던 것처럼 import로 얌전히 받아왔다.
'{ content: string; ref: MutableRefObject<any>; }' 형식은 'IntrinsicAttributes' 형식에 할당할 수 없습니다.
'IntrinsicAttributes' 형식에 'content' 속성이 없습니다.ts(2322)
어이가 없었다... p 태그, ul 태그 등에서 갑자기 해당 오류가 발생했다. git clone을 받아오니까 생긴 오류인데 도무지 알 수가 없어서 vscode를 껐다가 켜보니 갑자기 오류들이 사라졌다 (ㅋㅋㅋ)
위처럼 평소처럼 테스트 하려고 했다.
이미지나 파일 같은 경우에는 raw가 아니라 form data로 서버로 보내야 한다. 백엔드분들이 보내주신 api 문서를 다시 한 번 살펴 보고 아래와 같이 테스트를 해 보았다.
잘 보내진다!
Key의 file이 text 형식으로 되어 있을 텐데 그걸 file로 바꿔주면 value 부분에 파일을 첨부할 수 있게 바뀐다.
headers 에는 로그인 후 발급받은 토큰을 넣었다.
(
showImages.map((src, idx) => {
return (
<div className="flex rounded-lg bg-slate-200 h-28" key={idx}>
<img className="m-auto" src={src}>
<button
onClick={() => handleDelete(idx)}
className="text-sm text-gray-500 p-[10px] rounded-2xl bg-white uploade">
Delete
</button>
map 함수를 사용했는데 key 값을 설정해 주지 않아 생긴 오류이다. 제일 상단의 컴포넌트에 key 값을 설정해 오류를 해결하였다.
Access to XMLHttpRequest at 'url' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:3000, *', but only one is allowed.
Failed to load resource: net::ERR_FAILED
Uncaught (in promise) AxiosError
내 컴퓨터에서 이미지를 클릭해 올리려고 하면 axios 에러가 떴다. 네트워크 오류라길래 백엔드 오류인가 싶었는데 포스트맨으로 테스트 해 봤을 땐 정상적으로 작동해서 이상했다.
<input
type="file"
className="real-upload"
accept="/image/*"
onChange={handleUpload} //삭제하면 정상작동함
/>
문제가 된 코드는 이미지 input
의 onChange
함수이다.handleUpload
는 Axios를 이용해 Form 전체의 데이터를 서버에 전송하는 함수이다. 그게 이미지 input
에 들어가 있기 때문에 요청이 로컬3000과 /* 이렇게 두 개로 보내져서 CORS가 난 거였다.
//이미지 업로드
const inserImg = async (e: ChangeEvent<HTMLInputElement>) => {
const reader = new FileReader();
const newFiles = e.target.files[0];
if (newFiles) {
reader.readAsDataURL(newFiles);
}
reader.onloadend = () => {
const previewImgUrl = reader.result;
console.log(previewImgUrl);
};
};
위와 같이 코드를 작성했을 때 e.target.files[0]
부분에서 ts2531 에러가 떴다. 구글에 찾아봐도 마땅한 해결방법이 나오지 않아 이것저것 시도해 보다가 아래와 같이 작성하여 해결하였다.
//이미지 업로드
const inserImg = async (e: ChangeEvent<HTMLInputElement>) => {
const reader = new FileReader();
if (e.target.files) {
reader.readAsDataURL(e.target.files[0]);
}
reader.onloadend = () => {
const previewImgUrl = reader.result;
console.log(previewImgUrl);
};
};
수정 전
const [searchProducts, setSearchProducts] = useState();
const handleOnChange = (e: React.FormEvent<HTMLElement>) => {
const value = e.currentTarget;
setSearchProducts(value);
console.log(value);
};
SetSearchProducts(value)
부분의 value
에서 에러가 발생했다.
const [searchProducts, setSearchProducts] = useState<
EventTarget & HTMLElement
>();
useState
에 해당 타입을 넣어주어 에러를 해결하였다.
document.querySelector('#but_name')!.textContent
뒤에 !를 붙여주면 해결 된다.
//셀렉터에서 클릭한 것 이름 저장
const [clickName, setClickName] = useState('');
const handleButClick = (e: React.MouseEvent<HTMLButtonElement>) => {
//클릭한 버튼의 id값이 들어옴
if (e.currentTarget.id) {
setClickName(e.currentTarget.id);
console.log(e.currentTarget.id);
}
//클릭한 버튼의 text 값 들어옴
if (e.currentTarget.textContent) {
setClickName(e.currentTarget.textContent);
console.log(e.currentTarget.textContent);
}
};
//...생략
<button
onClick={handleButClick}
id="MONITOR"
className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900"
>
Monitor
</button>
useState(null)
로 되어 있던 것을 useState("")
로 변경하자 에러를 해결할 수 있었다!
커스텀 셀렉터를 클릭하면 div 가 나오고, div 안엔 버튼들이 들어가있다. 버튼을 클릭하면 커스텀 셀렉터가 닫히고, 기본값으로 설정된 '대분류 선택' 텍스트가 클릭한 버튼의 이름으로 바뀐다.
그리고 이렇게 바뀐 값을 axios.get
요청의 params
에 type
의 value
로 보낸다.
예를들어, 대분류 셀렉터에서 Monitor 버튼을 클릭했다면 params: {type : Monitor}
이런 식으로 보내야 하는 것이다. 그 후 데이터를 받아와 소분류 셀렉터에서 타입이 모니터인 데이터들을 불러와 띄워줄 계획이었다.
이렇게 에러가 뜨지만 않았다면 말이다... axios.get
으로 보낼 때 글자가 깨져서 보내지는 문제였다. 구글에 검색해보니 index.html
에 UTF-8
설정을 해 주면 된다고 하는데 당연히 기본적으로 설정되어 있었다. 팀원분께서 decodeURI()
라는 것을 알려주셔서 찾아봤다. (링크)
깨지는 글자들을 encodeURI()
에 넣어서 보내면 글자가 깨지지 않는 것이다! 나와 같은 경우엔 대분류 셀렉터의 버튼은 Monitor
로 맨 앞글자만 대문자였고, 서버에 보낼 때 모두 대문자로 보내야하기 때문에 .toUppercase()
를 사용했다.
//url 디코딩
const encoded = encodeURI(clickName);
//대문자로 변환
const UpperText = encoded.toUpperCase();
//카테고리 셀렉터의 clickname 값을 받아와서 그 값을 api 요청할 때 url 로 넣어서 보내기
useEffect(() => {
axios
.get(`url`, {
params: { type: UpperText },
})
.then((res) => res.data())
.then((categorie) => {
setCategorie(categorie.name);
})
.catch((err) => console.log(`clickName err =>`, err));
}, [spread]);
새로운 에러가 뜨긴 했지만 글자가 깨지는 것 에러는 해결 완료했다!
프론트에서 http-proxy-middleware
를 설치하고 setupProxy.js
에서 프록시 설정을 해서 서버와 연결하여 테스트를 진행하고 있었다. 그런데 계속 뜨는 저 CORS 오류가 날 미치게 만들었다.
요청 헤더에 origin
이 null
들어가고 있는 것 때문에 응답에 Access-Control-Allow-Origin
가 null
과 *
로 같이 들어오고 있었다. (지금 다시 확인해 보니 null이 아니라 로컬3000으로 들어오고 있다! 아무튼 오류 뜨는 건 똑같음...)
로컬 환경이어서 뜨는 에러라고 생각했기 때문에 작성한 코드를 push 하고 개발 url 로 들어가서 테스트를 해 봤다...!
된다...
하지만 이대로는 안 된다. 어떻게든 CORS 에러를 해결해야 한다. (아래로 이어짐)
대분류 셀렉터에서 버튼을 클릭하고
-> 버튼의 text 값을 params
에 type
으로 넣어 보내고
-> 소분류 셀렉터를 누르면 버튼의 params
로 보낸 값과 같은 type
의 제품들만 보여주는 기능 구현
을 하고 싶었는데 소분류 셀렉터를 누르면 위와 같은 에러가 발생했다. 개발 url 로는 잘 되는 상황이었지만 이 에러를 해결하지 않으면 코드를 짤 수가 없는 상황이었다.
const data = () => {
axios
.get(`https://codetech.nworld.dev/api/products/review-search`, {
params: { type: upperText },
})
.then((res) => {
console.log(`res.data`, res.data);
setCategorie(res.data);
})
.catch(function (error) {
if (error.response) {
console.log(`error.response.data`
... 생략
문제의 코드이다.
에러가 발생하는 부분은 url이었다.
setupProxy로 CORS 우회를 해 주고 있는데 url 부분에 전체로 적어서 생긴 문제였다... 아래처럼 수정하니 에러를 해결할 수 있었다.
const data = () => {
axios
.get(`/api/products/review-search`, {
params: { type: upperText },
})
.then((res) => {
console.log(`res.data`, res.data);
setCategorie(res.data);
})
.catch(function (error) {
if (error.response) {
console.log(`error.response.data`,
... 생략
대분류 셀렉터를 클릭하여 get으로 버튼의 text를 보내고, 그 값과 같은 type들을 res.data 로 받아와서 소분류 셀렉터 클릭시 리스트를 map 으로 보여주는 코드를 작성하려고 했다.
//get으로 받아온 데이터 저장
const [categorie, setCategorie] = useState(null);
//소분류 셀렉터 클릭시 대분류에서 선택한 text 값과 일치하는 type의 data list 를 가져옴
const handleSelectClick = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
setSpread(!spread);
};
const data = () => {
axios
.get(`/api/products/review-search`, {
params: { type: upperText },
})
.then((res) => {
console.log(`res.data`, res.data);
setCategorie(res.data);
console.log(`categorie`, categorie);
})
.catch(function (error) {
if (error.response) {
console.log(`error.response.data`, error.response.data);
console.log(`error.response.status`, error.response.status);
console.log(`error.response.headers`, error.response.headers);
} else if (error.request) {
console.log(`error.request`, error.request);
} else {
console.log('Error', error.message);
}
console.log(`error.config`, error.config);
})};
//data 받아와서 map으로 뿌려주는 코드
{categorie === null ? (
<button className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900">
제품이 없습니다
</button>
) : (
categorie.map((product, idx) => {
return (
<button
key={idx}
id={product.name}
onClick={handleButClick}
className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900"
>
{product.name}
</button>
);
})
)}
이렇게 작성했더니 map으로 뿌려주는 categorie.map
부분에서 계속해서 에러가 발생했다. 게다가 console.log('categorie', categorie)
도 null 로 찍히고 있었다.
//get으로 받아온 데이터 저장
const [categorie, setCategorie] = useState<string[] | null>(null);
//소분류 셀렉터 클릭시 대분류에서 선택한 text 값과 일치하는 type의 data list 를 가져옴
const handleSelectClick = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
setSpread(!spread);
await axios
.get(`/api/products/review-search`, {
params: { type: upperText },
})
.then((res) => {
console.log(`res.data`, res.data);
setCategorie(res.data);
console.log(`categorie`, categorie);
})
.catch(function (error) {
if (error.response) {
console.log(`error.response.data`, error.response.data);
console.log(`error.response.status`, error.response.status);
console.log(`error.response.headers`, error.response.headers);
} else if (error.request) {
console.log(`error.request`, error.request);
} else {
console.log('Error', error.message);
}
console.log(`error.config`, error.config);
});
};
제품 등록하기 모달에서 전송하기 버튼을 누르면 화이트라벨 페이지가 뜨면서 bad request와 400 err 를 내뿜었다.
이것저것 찾아보다가 이상한 점을 발견했다. 전송하기 버튼을 클릭했을 때 작동되는 axios
의 url
은 '/api/products'
인데 여기서 products에서 s를 빼도 '/api/products'
페이지 주소가 뜨며 오류라고 나오는 것이었다.
axios
부분말고 다른 곳에/api/products
라고 적은 곳이 있나 찾아보았더니... form
이 범인이었다...! action
부분을 주석처리하니 화이트라벨 페이지가 나오지 않게 되었다.
<form
id="form-data"
className="w-2/3"
// method="post"
// action="/api/products"
encType="multipart/form-data"
>
ServletException: org.apache.tomcat.util.http.fileupload.impl.InvalidContentTypeException: the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is application/json] with root cause
화이트라벨 에러를 해결하고 이제 제품등록 모달의 formData
를 전송하는 일만 남았다. 전송을 하려고 하니 제대로 데이터가 보내지지 않았다. 서버 로그를 확인해 보니 위와 같이 떴고, 뭐가 문제인가? 잘 이해가 되지 않아 백엔드 팀원분과 이야기를 나눠보니 Content-Type': 'multipart/form-data
의 문제인 것 같았다.
const productData: any = {
name: name,
tpye: upperText,
detail: detail,
};
console.log(`productData`, productData);
const handleSubmitImg = async () => {
const formData = new FormData();
formData.append(
'file',
new Blob([uploadImg] as any, { type: 'application/json' })
);
formData.append(
'request',
new Blob([JSON.stringify(productData)] as any, {
type: 'application/json',
})
);
console.log(`formData`, formData);
const submitForm = await selectProductImg(formData);
switch (submitForm.status) {
case 200:
navigate('/review/write');
location.reload();
break;
case 415:
console.log('실패');
}
};
//api 호출 함수
export const selectProductImg = async (data: any) => {
try {
const submitImg = await axios.post('/api/products', data, {
headers: {
Authorization: initialToken,
},
});
return submitImg;
} catch (err: any) {
return err.response;
}
};
formData.append('request' ...생략)
의 타입과 formData.append('file' ...생략)
의 타입은 제대로 작성했다고 생각한다. 그럼 도대체 뭐가 문제일까? 구글링을 해 보니 form
태그에 encType="multipart/form-data"
를 해주지 않아 생긴 문제인 것 같았다. (확실하지 않음)
제품 이미지, 디테일, 작성자, 이름, 타입이 잘 들어가고 있다.
type 부분이 제대로 들어가지 않고 있다.
이름, 타입, 디테일 부분을 따로따로 보내고 있는 것도 아니고 productData
객체로 묶어서 한 번에 request
라는 이름으로 보내고 있는데 타입만 null
값으로 들어가는 건 이상하다.
form
의 encType="multipart/form-data"
설정 문제인가? 혹은 헤더에 'Content-type': 'multipart/form-data'
을 안 넣어서 문제인가? 이것저것 시도해 봤지만 모두 똑같았다.
마이페이지에서 이미지 업로드, 수정 부분을 구현하신 팀원분에게 여쭤봤는데... ... ... ... productData
의 type
에 오타가 있었다.
const productData: any = {
name: name,
type: upperText,
detail: detail,
};
진짜 에바임... 아무튼 에러 해결...! ㅎ
응답으로 들어온 데이터를 categorie
에 저장하고 .map()
를 사용하여 데이터를 뿌려주는 코드를 작성하니 위와 같은 에러가 나타났다. 객체로 들어온 데이터에 .map()
을 사용하려고 하니까 생긴 문제였다.
interface ICategorieData {
id: number;
name: string;
}
const [categorie, setCategorie] = useState<ICategorieData | null>(null);
... 생략
return (
...생략
{categorie === null ? (
<button className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900">
제품이 없습니다
</button>
) : (
categorie.map((product, idx) => {
return (
<button
key={idx}
onClick={handleButClick}
className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900"
>
{product.name}
</button>
);
})
)}
)
const [categorie, setCategorie] = useState<ICategorieData[] null>([]);
useState
부분만 수정해주면 되는 문제였다. .map()
는 배열에서만 사용이 가능하고, 서버에서 받아온 데이터(객체)를 categorie
라는 배열에 담아 사용하는 상태였기 때문에 인터페이스와 기본값으로 []을 넣어주면 되는 거였다.
//이메일 인증 번호 작성 후 post 요청
const emailNumCheckClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
const emailNumData = {
email: email,
code: certification,
};
const emailCheckReq = await postEmailCertificationCheck(emailNumData);
switch (emailCheckReq.status) {
case 201:
alert('이메일 인증이 완료되었습니다');
console.log('이메일 인증이 완료되었습니다');
break;
case 401:
alert('인증번호가 일치하지 않습니다');
console.log('인증번호가 일치하지 않습니다');
break;
}
};
이메일을 입력하고 인증번호 받기를 클릭하면 작성한 이메일로 인증번호가 오는 코드이다. 인증번호 받기 버튼 클릭시 + 받은 인증번호를 input에 입력 후 인증완료 버튼을 누를 때 각각 api를 호출하는데 api가 정상적으로 호출되어 인증번호 코드가 메일로 오는데 alert 창은 뜨지 않았다.
이유는 case 200: // case 404:
로 적어야 하는데 잘못 적었기 때문이다. 응답으로 들어오는 상태 코드를 잘 봤어야 했는데 내 불찰이다...
//get으로 받아온 데이터 저장
const [categorie, setCategorie] = useState<ICategorieData[] | null>([]); //수정한 부분
//소분류 셀렉터 클릭시 대분류에서 선택한 text 값과 일치하는 type의 data list 를 가져옴
const handleSelectClick = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
setSpread(!spread);
try {
const res = await axios.get(`/api/products/review-search`, {
params: { type: upperText },
});
setCategorie(res.data);
} catch (error: unknown) {
handleError(error);
}
};
//리턴문
{categorie === null ? ( //수정한 부분
<button className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900">
제품이 없습니다
</button>
) : (
categorie.map((product, idx) => {
return (
<button
key={idx}
onClick={handleButClick}
value={product.id}
className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900"
>
{product.name}
</button>
);
})
)}
분명 categorie
가 null
이 아니라면 '제품이 없습니다'가 보이겠끔 코드를 작성했는데 다시 확인해 보니 삼항 연산자의 true 부분이 작동하지 않는 것을 확인했다.
이유는 get
요청으로 받아온 데이터를 저장하는 카테고리에 타입을 null
로 해뒀기 때문이다. const [categorie, setCategorie] = useState<ICategorieData[] | null>([]);
대분류에서 선택한 tpye
에 제품 데이터가 없다면 카테고리는 빈배열([])이지 null
이 아니기 때문에 삼항연산자의 true 부분이 보이지 않았던 것이다.
나는 간단하게 카테고리에 length을 사용하여 카테고리의 길이가 0 이라면 '제품이 없습니다'를 띄우고, 그렇지 않다면 제품 데이터를 보여주는 식으로 코드를 수정해야겠다고 생각했고 아래와 같이 수정하였다.
//get으로 받아온 데이터 저장
const [categorie, setCategorie] = useState<ICategorieData[] | []>([]);
//소분류 셀렉터 클릭시 대분류에서 선택한 text 값과 일치하는 type의 data list 를 가져옴
const handleSelectClick = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
setSpread(!spread);
try {
const res = await axios.get(`/api/products/review-search`, {
params: { type: upperText },
});
setCategorie(res.data);
} catch (error: unknown) {
handleError(error);
}
};
//리턴문
{categorie.length === 0 ? (
<button className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900">
제품이 없습니다
</button>
) : (
categorie.map((product, idx) => {
return (
<button
key={idx}
onClick={handleButClick}
value={product.id}
className="flex items-center w-full px-4 pt-2 pb-3 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900"
>
{product.name}
</button>
);
})
)}
input
으로 포커스가 넘어가는 게 아니라 냅다 이메일 인증 받기 위해 클릭해야 하는 (클릭시 post api 요청이 가는) 버튼이 실행 됨// handleEnter
const handleEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.code === 'Enter') {
onSubmit;
}
};
//비밀번호
<input
type={passwordType}
className="absolute top-0 w-full h-full px-6 pt-3 text-base font-medium bg-transparent outline-none peer/password input-ani"
value={password}
name="password"
onChange={onChangePassword}
onKeyDown={handleEnter}
></input>
<label
className={`absolute font-medium top-4 left-6 text-gray-500 duration-200 pointer-events-none peer-focus/password:-translate-y-2.5 peer-focus/password:text-xs ${
password.length !== 0 && 'peer-valid/password:-translate-y-2.5 peer-valid/password:text-xs'
}`}
>
//비밀번호 확인
//위와 코드 같음
<form>
태그 안에 submit
타입의 버튼이나, 함수가 있다면 <form>
태그 밖으로 이동 시킨다.<button>
태그는 타입을 지정해주지 않으면 기본적으로 가지게 되는 타입이 있다. (참고) <form>
태그 안의 버튼은 타입을 지정해주지 않으면 submit
타입이 되어버리기 때문에 아무 타입도 설정하지 않았던 버튼 태그들에 <button type='button'>
타입을 지정해 준다. input
에서 텍스트를 작성하고 엔터를 누르면 다음 인풋으로 이동하게끔 만드는 것이 목표이기 때문에 onKeyDown()
에 함수를 수정한다. // handleEnter
const handleEnter = (
e: KeyboardEvent<HTMLInputElement | HTMLButtonElement>,
next: string
) => {
const key = e.key || e.keyCode;
if (key === 'Enter' || key === 13) {
if (next === 'signUpSubmit') {
onSubmit;
} else {
document.getElementById(next)?.focus();
}
}
};
//리턴문
<form>
<input
type="text"
id="nickname"
className="absolute top-0 w-full h-full px-6 pt-3 text-base font-medium bg-transparent outline-none peer/email input-ani"
value={nickname}
name="nicmname"
onChange={onChangeName}
onKeyDown={(e) => handleEnter(e, 'email')}
></input>
//이메일
<input
type="text"
id="email"
className="absolute top-0 w-full h-full px-6 pt-3 text-base font-medium bg-transparent outline-none peer/email input-ani"
value={email}
name="email"
onChange={onChangeEmail}
onKeyDown={(e) => handleEnter(e, 'but01')}
></input>
KeyboardEvent<HTMLInputElement | HTMLButtonElement>
으로 지정한 이유는 엔터의 이동 경로가 닉네임 작성 -> 엔터 -> 이메일 작성 -> 엔터(버튼 눌림) -> 인증번호 입력 -> 엔터(버튼 눌림) -> ...
이런 식이라 인풋과 버튼을 와리가리 해서 하나의 함수로 사용하기 위해 <HTMLInputElement | HTMLButtonElement>
로 지정하였다.