ㅇㅇㅈ Blog

프론트엔드 수행중

0%

React TS 에서 SVG Import Error

JS로 만든 react project를 TS로 변환 하는 과정에서
SVG 파일을 Import 하는데 에러가 발생 했다.

  1. src 디렉토리 안쪽에 custom.d.ts파일을 만들고, 아래와 같이 작성 해준다
1
2
3
4
5
6
7
8
// custom.d.ts 

declare module "*.svg" {
import React = require("react");
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}
  1. tsconfig.jsonsrc/custom.d.ts 를 추가로 작성 해 준다
1
"include": ["src", "src/custom.d.ts"]
  1. Import ~
1
import { ReactComponent as DeleteIcon } from "../../asset/delete.svg";

SVG 아이콘에 onClick Event Type

생각 없이 SVG에 onClick 이벤트 타입으로 버튼 엘리먼트로 줬더니 에러가 떳다.

1
2
3
4
5
6
7
<DeleteIcon
width="12px"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
deleteTag();
}}
/>

엘리먼트 타입없이 그냥 MouseEvent만 타입으로 지정해주면 된다!

JWT 로그인 방식

서버에 로그인을 하면 토큰을 발급해주어
사용자가 서비스를 이용할때 이 토큰을 요청에 포함시켜 사용자를 인증한다.
근데 이 토큰엔 만료시간이 있다
이 프로젝트에서는 이 토큰의 만료시간이 15분이다
그래서 토큰이 만료되면 리프레시 토큰으로 다시 새로운 access_token을 발급 받아야 한다

오잉…..

access_token이 만료되면.. 리프레시 토큰으로 새롭게 발급 받으라고!?

일단 로그인할때 response 받은 access_tokenrefresh_token을 저장하자

시나리오 1.

  1. 로그인 할 때 발급 받은 access_token은 리덕스에, refresh_token은 쿠키에 저장한다
  2. hoc를 이용해 access_token이 만료되면 새롭게 발급받고 토큰이 필요한 컴포넌트에 props로 넘겨준다

react-cookie라는 라이브러리를 통해 쿠키에 쉽게 저장 할 수 있었다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// settingSessions.js
import { Cookies } from "react-cookie";
const cookies = new Cookies();

// 로그인 할때 받은 리프레시토큰을 인자로 받아 쿠키에 저장한다
export const setRefreshToken = (refreshToken) => {
const today = new Date();
const expireDate = today.setDate(today.getDate() + 7);

return cookies.set("refresh_token", refreshToken, {
sameSite: "strict",
path: "/",
expires: new Date(expireDate),
});
};

// 쿠키에 저장된 토큰을 가져온다
export const getCookieToken = () => {
return cookies.get("refresh_token");
};

// 쿠키에 저장된 토큰을 삭제한다
export const removeCookieToken = () => {
return cookies.remove("refresh_token", { sameSite: "strict", path: "/" });
};

Read more »

Final-Project-3

07.24 Project

파이널이 끝났다.

호기럽게 시작한 파이널은 만족스럽지 못하게 끝났다

기간동안 매일같이 파이널만 붙들고 코드를 치다 보니
블로그를 작성할 시간도 없었다. 물론 짬을 내면 쓸 수 있었겠지만…

파이널은 키워드를 통한 뉴스검색을 하고 뉴스를 스크랩하고, 키워드를 저장하고..
뭔가 미니프로젝트와 비슷하단 생각하며 시작했는데 막상 들어가보니 훨씬 복잡하고 어려웠다.

api통신이 많아지고 그 통신마다 서로 연관되어 데이터를 조회하는 것이 많다보니
각각의 에러케이스 처리와 상태관리에 많은 어려움이 있었다.

또.. 토큰을 이용한 로그인 방식도 처음이다 보니
토큰관리에도 애를 먹었다.
어떤 페이지에 접속하면 refresh 토큰이 10번씩 호출되어
연계 기업쪽 팀장님이 refresh 호출이 너무 많다고 하소연 하셨을 정도니

마지막에는 어떻게든 더 작성해야겠다는 마음때문에 팀원들과 소통없이 코드 작성하고
git에 올렸더니 파일이 꼬여 약간 멘붕도 왔다.
코드양이 많지 않아 금방 수정하긴 했지만..

어쨌든 끝이 났는데 그렇게 홀가분하지는 않다
항상 무언가 끝나고나면 좀 더 열심히했으면.. 하는 후회를 하는건 언제쯤 고칠 수 있을까

프로젝트하면서 느낀게 비슷한 내용을 작성하는데 항상 어떻게 해야할지 몰라 끙끙댔었는데..
머릿속에 지우개가 들어서인지 응용력이 떨어져서인지 참..

이제 작성했던 코드 좀 돌아보면서 정리하는 시간이 좀 필요한거 같다

Final-Project-02

2022.07.06

스타일 작업을 하는중..
스타일드 컴포넌트로 테마설정도 하고…
switch case로 클릭하는거에 따라 보기타입이 바뀌게도 설정 해 줄 수 있다

나중에 코드 정리해야지

일단 지금은 카테고리 버튼을 누르면 페이지가 이동되고
이동된 페이지에 상단에 클릭된 버튼이 활성화 되어 있다

문제는 버튼들이 가로로 나열되어있고 overflow scroll인데

마지막 카테고리를 클릭하면 스크롤하기 전에 해당 버튼이 활성화 된게 안 보인다는 점..

일단 아이디어는 주소로 넘어오는 params와
버튼의 id를 비교해서 같으면 색상을 활성화 해주는거다

그래서 검색 시작

useRef 와 scrollIntoView 라는걸 사용하면 클릭하면 참조값에 스크롤이 된다고 해서 작성 해봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const CategoryButton = () => {
const params = useParams();
const btnRef = useRef(null);

useEffect(() => {
btnRef.current.scrollIntoView({
behavior: "smooth",
});
}, []);

return (
<QuickButtonWrap id="scroll-container">
{mainCategory.map((category) => {
return (
<RadiusButton
orange={params.id === category.id}
ref={btnRef}
>
{category.category}
</RadiusButton>
);
})}
</QuickButtonWrap>
);
};

export default CategoryButton;

이렇게 작성했더니 맵을 돌면서 마지막 버튼에만 활성이 되는 것이다

근데 여럿 찾아본 블로그와 다른 점은.. 블로그에서는 ref를 부모로부터 받아와 참조를 하는거지만
여기서는 그렇지가 않다 부모 자식 관계도 아니기 때문에….

그리고 다시 폭풍 검색

찾다보니 ref를 배열로 저장할 수 있더라

1
ref={(el) => (btnRef.current[idx] = el)}

이런식인데
맵을 돌면서 index값으로 current를 찾아 내는 방식인거 같다!?

그래서 일단 배열로 저장하고 콘솔로 찍어 보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const CategoryButton = () => {
const params = useParams();
const btnRef = useRef([]); // 배열로 설정

useEffect(() => {
btnRef.current.scrollIntoView({
behavior: "smooth",
});
}, []);

return (
<QuickButtonWrap id="scroll-container">
{mainCategory.map((category,idx) => {
return (
<RadiusButton
orange={params.id === category.id}
ref={(el) => (btnRef.current[idx] = el)} // ref를 배열로 저장
>
{category.category}
</RadiusButton>
);
})}
</QuickButtonWrap>
);
};

export default CategoryButton;

각 버튼의 ref가 배열로 저장된다.

그럼 이제 btnRef를 for문을 돌면서 해당 인덱스에 있는 텍스트와 params를 비교해서
scrollIntoView를 해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const CategoryButton = () => {
const params = useParams();
const btnRef = useRef([]);

useEffect(() => {
btnRef.current.forEach((item, idx) => { // 배열을 순회하고
if (item.innerText.toLowerCase() === params.id) { // innertext와 비교를 했다..
btnRef.current[idx].scrollIntoView({ // true 일시 해당 index의 ref로 scrollIntoView가 된다
behavior: "smooth",
});
}
});
}, []);

return (
<QuickButtonWrap id="scroll-container">
{mainCategory.map((category, idx) => {
return (
<Link
to={`/quick${category.path}`}
key={category.id}
id={category.id}
>
<RadiusButton
orange={params.id === category.id}
ref={(el) => (btnRef.current[idx] = el)}
>
{category.category}
</RadiusButton>
</Link>
);
})}
</QuickButtonWrap>
);
};

export default CategoryButton;

잘 작동 된다!

axios

axios 라이브러리에는 많은 기능이 들어있는거 같다..
그 동안 정말 axios로 요청만 날리고 있었다

axios.create()를 통해 baseURL, header 등을 설정해 놓을 수 있다

1
2
3
4
5
6
7
import axios from "axios";

const clientServer = axios.create({
baseURL: process.env.SERVER,
});

export default clientServer;

사용 할 때는 axios도 임폴트가 필요 없다
clientServer만 임폴트 해주면 된다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

import clientServer from "./baseUrl";

const getMasterData = async () => {
try {
const response = await clientServer({
url: "/master",
});
if (response.status === 200) {
const data = await response.data;
console.log(data);
return data;
}
} catch (e) {
console.log(e);
}
};

header에 토큰 값도 미리 설정 해 놓을 수 도 있다

URLSearchParmas

https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams

공백이나 특수 문자는 url에 인코딩한 형태로 들어가야한다

encodeURI()를 사용할 수도 있지만 번거롭다

URLSearchParams를 이용해서 변환해 줄 수 있다

1
2
3
var params = new URLSearchParams({'foo' : '1', 'q': '안녕 하 세 요 ^^'})
params.toString()
// 'foo=1&q=%EC%95%88%EB%85%95+%ED%95%98+%EC%84%B8+%EC%9A%94+%5E%5E'

URLSearchParams에 객체의 형태로 넣어 줬는데
주소에서 사용 할 수 있는 쿼리파라미터에 키 밸류 값으로 자동으로 해준다

Final-project 01

2022.06.25 새벽..

  • 본격적으로 final project가 시작되었다..
    아직 기획단계라서 디자인도 나오진 않았지만
    미리 API를 받아 코드를 작성 해 볼 수 있었다..

  • 맨 처음 이 프로젝트를 선택 할 때 많은 데이터를 다룰 수 있어서 선택 했는데
    럴수가.. 데이터가 너무 많다

검색을 하면 데이터에서 맞는 키워드를 화면에 띄워줘야 하는데
데이터양이 많아서 쓰로틀링이 너무 걸린다..
어떻게 해결해야 하나
저 에러 메세지 조차 모든 데이터를 필터링 한 거도 아니다
기업측에서 보내준 데모사이트에서는 바로바로 필터링돼서 화면에 출력 되던데
어떻게 하는거지..?

lodash 라이브러리를 사용해서 filter하면 좀 더 빠르다 해서 해봤는데
차이가 있는지 모르겠다 -. -

2022.06.26

  1. 어찌어찌어찌 키워드를 에러 없이 띄우는데 까지는 성공을 했다
  2. 처음보다는 낫지만 아직 성능이슈가 있다 첫 글자를 입력하면 쓰로틀링이 걸린다
  3. new Map() 이나 Set()을 이용해서 필터링을 하는 방법을 공부 해봐야겠다
  4. 정규식은 어렵다

정규식에 변수를 넣기 위해 검색 해보았다

1
2
const regex = new RegExp(`\s(${keyword})+|(${keyword})+`, "im");
return regex.test(item.name);

일반적인 자바스크립트 보간법으로 변수를 넣어 줄 수 있다.

첫 시작과 중간에 같은 단어가 있으면 첫 단어를 찾지 못한다는 점이 문제였다

|를 이용해서 정규식을 추가해 줄 수 있었다..

커스텀 훅을 사용하면서 생긴 erro case

api 호출 부분과 무한스크롤 부분을 컴포넌트에서 분리 하는 과정에서
이런 에러가 발생했다

검색 해보니

  1. react-dom의 버전이 Hook을 사용할수 있는 버전인 16.8.0보다 낮을때
  2. Hooks를 호출 할 때는 함수형 component의 최상위 부분에서 호출
  3. 동일한 앱에 둘 이상의 사본이 있을때

일단 1번 케이스는 해당이 안되고
3번 케이스도 npm i 로 새로 설치했는데도 아니었다

그럼 2번이 문제인데.. 분명 hook은 컴포넌트 최상단에서 호출 하고 있는 중인데..

일단 분리한 코드는

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useState, useEffect, useRef, useCallback } from "react";
import { useDispatch } from "react-redux";
import axios from "axios";

const GetDataHooks = (keyword, page, setPage) => {
const [newsData, setNewsData] = useState([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(false);
const [error, setError] = useState(false);

const API_KEY = process.env.REACT_APP_API_KEY;

const dispatch = useDispatch();

useEffect(() => {
setNewsData([]);
}, [keyword]);

useEffect(() => {
if (keyword && !loading) {
const searchTimeout = setTimeout(() => {
setLoading(true);
setError(false);
let cancel;
axios({
method: "GET",
url: `https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=${API_KEY}`,
params: { q: keyword, sort: "newest", page },
cancelToken: new axios.CancelToken((c) => (cancel = c)),
})
.then((res) => {
setNewsData((prev) => [...prev, ...res.data.response.docs]);
setHasMore(res.data.response.docs.length > 0);
setLoading(false);
})
.catch((e) => {
if (axios.isCancel(e)) return;
setError(true);
});
dispatch({ type: "SEARCH_HISTORY", payload: keyword });
}, 500);
return () => {
clearTimeout(searchTimeout);
};
}
}, [keyword, page]);

const observer = useRef();
const lastCardNewsRef = useCallback(
(node) => {
console.log("visible");
if (loading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setPage((page) => page + 1);
// 문제의 호출
GetDataHooks(keyword, page, setPage,)
}
});
if (node) observer.current.observe(node);
},
[loading, hasMore]
);

return { newsData, loading, lastCardNewsRef };
};

export default GetDataHooks;

keyword, page, setPage를 인자로 받아서 안에서 api를 호출 하여
newsData와, loading값, 무한스크롤 함수를 return 해주게 작성했다

문제는.. 커스텀훅 안에서 커스텀훅을 호출 하고 있어서 이런 에러가 나는 거였다……

저 GetDataHooks()를 지워 주니 에러가 사라졌다 ..

Ref

부모컴포넌트에서 자식컴포넌트의 DOM엘리먼트를 참조 하고 싶을때

1
2
3
4
5
6
7
8
9
10
// NewsList Component
const NewsList = ({ news, lastCardNewsRef }) => {
return (
<Container>
{news.map((newsData, idx) => {
return <News ref={lastCardNewsRef} />;
})}
</Container>
);
};
1
2
3
4
5
6
7
8
9
10
11
// News Component

const News = (props) => {
return (
<Card ref={props.ref}>
<h2>{props.headline}</h2>
</Card>
);
};

export default News;
  • 함수 컴포넌트는 인스턴스가 없기 때문에 ref어트리뷰트를 사용할 수 없다

https://ko.reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components

Summary

ref

  • ref는 특정 DOM (Html)을 참조 할 때 사용 한다
  • ref는 리액트 예약어이다 (ex: key, className)
  • 함수 컴포넌트는 인스턴스가 없기 때문에 ref어트리뷰트를 사용할 수 없다
  • 간단하게는 ref의 이름을 myRef 같은 이름으로 수정하여 해결 할 수 있다
  • 또는 리액트훅 forwardRef를 이용해 ref 참조 값을 사용 할 수 있다

How to solve

  1. ref 이름 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// NewsList Component
const NewsList = ({ news, lastCardNewsRef }) => {
return (
<Container>
{news.map((newsData, idx) => {
return (
<News myRef={lastCardNewsRef}
✅ // myRef 이름으로 컴포넌트에 전달
/>
);
})}
</Container>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
// News Component

const News = (props) => {
return (
<Card ref={props.myRef}>
✅ // props로 myRef
<h2>{props.headline}</h2>
</Card>
);
};

export default News;
  1. forwardRef hooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// NewsList Component
const NewsList = ({ news, lastCardNewsRef }) => {
return (
<Container>
{news.map((newsData, idx) => {
return (
<News ref={lastCardNewsRef}
✅ // ref 그대로 사용
/>
);
})}
</Container>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// News Component
import React, { forwardRef } from 'react';

const News = forwardRef((props, ref) => {
// forwardRef 사용
return (
<Card ref={ref}>
✅ // ref 사용
<h2>{props.headline}</h2>
</Card>
);
});

export default News;

date-fns

미니프로젝트 진행 중 데이터 중 날짜 데이터가 있어서 날짜를 가공하기 위해 date-fns 라이브러리를 사용해보기로 했다

1
$ npm i date-fns

format()

api로 받아오는 데이터는

이런식으로 저장되어있다

기본적으로 date를 원하는 형식으로 출력할수있는 메소드 format()이 있다
format()은 2개의 인수가 필수고 option으로 다른 값들을 넣어 줄 수 있다
첫 번째는 변경할 date
두 번째는 원하는 format 이다

1
2
format(new Date(), 'yyyy.MM.dd HH:mm')
// 2022.06.16 02:47

받아온 데이터를 yyyy.MM.dd HH:mm형식으로 바꿔보자

1
format(pub_date, 'yyyy.MM.dd HH:mm')

으로 작성 했더니 error를 뱉어 냈다

v2.0.0-beta 부터 문자열을 허용하지 않는다고 하면서 parseISO를 사용해달라고 한다

1
format(parseISO(pub_date), 'yyyy.MM.dd HH:mm')

이렇게 작성해 줬더니 잘 된다.

differenceInDays() , differenceInHours()

데이터가 작성된 날짜와 현재의 날짜를 비교해서 얼마전에 작성되었는지도 출력해주기 위해 differenceInDays() , differenceInHours() 도 사용해 주었다

두 메소드 다 비교할 날짜 2개를 인수로 넣어 준다

1
2
3
4
5
const result = differenceInHours(
new Date(2014, 6, 2, 19, 0),
new Date(2014, 6, 2, 6, 50)
)
//=> 12

differenceInDays()를 사용하면 작성된 날짜와 현재 날짜가 같으면 0이 반환 되기 때문에
differenceInHours()를 사용해 날짜가 같으면 시간으로 표시 해 준다

함수로 만들어 조건에 맞게 출력할 수 있도록 해준다

1
2
3
4
5
6
7
8
9
10
11
const dateFunc = (pub_date) => {
if (Math.abs(differenceInDays(parseISO(pub_date), new Date())) > 0) {
return (
Math.abs(differenceInDays(parseISO(pub_date), new Date())) + ' 일 전'
);
} else {
return (
Math.abs(differenceInHours(parseISO(pub_date), new Date())) + ' 시간 전'
);
}
};