๊ฑด๊ฐ ์ฑ๋ฆฐ์ง ํ๋ซํผ, ํ๋ฃจ์กฐ๊ฐ
๐ ํ๋ฃจ์กฐ๊ฐ ๊ตฌ๊ฒฝํ๊ธฐ
- "์ฝ๋ก๋ ๋๋ฌธ์ ๋ ๋ฌด๊ธฐ๋ ฅํ ๋, ์ฒด๋ ฅ๊ณผ ๊ฑด๊ฐ์ ์๊ฐํด์ ๋ญ๊ฐ๋ ํด์ผ ํ ๊ฒ ๊ฐ์๋ฐ.. ์์ง๋ฐ์ฝ? ์์ฌ์ผ์ผ?"
- " ์ด๋ป๊ฒ ํ๋ฉด ์คํธ๋ ์ค๋ ๋ ๋ฐ์ผ๋ฉด์, ๊ฑด๊ฐํ ์ต๊ด์ ๋ง๋ค ์ ์์๊น?"
- ํน์ ์ฌ๋ฌ๋ถ์ ์ด์ผ๊ธฐ๋ ์๋๊ฐ์? ์ด๋ฅผ ์ํด ๊ธฐํํ ๊ฒ์ด ๋ฐ๋ก ๋ฐ๋ก !!
- ๊ฑด๊ฐ์ฑ๋ฆฐ์ง ํ๋ซํผ ํ๋ฃจ์กฐ๊ฐ์ ๋๋ค!!
์์ ์ด ์ํ๋ ๋ชฉํ์ ๋ง๋ ์ฑ๋ฆฐ์ง๋ฅผ ์ ์ฒญํ๊ณ , ์๋ก๋ฅผ ์์ํ๋ค๋ณด๋ฉด ์ด๋์ ๋ชฉํ ๋ฌ์ฑ!
๋ณธ์ธ์ด ์ํ๋ ์ฑ๋ฆฐ์ง๊ฐ ์๊ฑฐ๋ ๊ธฐ๊ฐ์ด ์ ๋ง๋๋ค๋ฉด ์ฑ๋ฆฐ์ง๋ฅผ ๋ง๋ค์ด ์งํํ ์ ์์ต๋๋ค ๊ธฐ๊ฐ์ ์งง๊ฒ ์งํํ์ฌ ์ฑ๊ณตํ๋ ์ต๊ด๋ ๊ธฐ๋ฅผ์ ์์ต๋๋ค
๋ค๋ฅธ ์ ์ ์ ์์์ ํ์ธํ๋ฉฐ ๋๋ ํ ์ ์๋ค ๋ผ๋ ์์ ๊ฐ์ ์ป๊ณ ์๋ก ์์ํ์ฌ ๋ชฉํ๋ฅผ ๋ฌ์ฑํ ์ ์์ต๋๋ค
์ฑํ ์ ํตํด ๋ชฉํ ๋ฌ์ฑ์ ๊ดํ ๊ฟํ์ด๋ ์์ํ ์ํต์ผ๋ก ์นํด์ง ์ ์์ต๋๋ค
๋ค๋ฅธ ์ ์ ๋ฅผ ์์ํ๊ฑฐ๋ ์ธ์ฆ์ท์ ์ฌ๋ฆฌ๋ฉด ์กฐ๊ฐ์ ๋ฐ์ ์ ์์ต๋๋ค
์ผ์ ์กฐ๊ฐ์ ๋ชจ์ผ๋ฉด ๊ตฌ์ฌ๋ก ๋ณ๊ฒฝ๋๋ฉฐ, ๊ตฌ์ฌ์ ๋ชจ์ผ๋ ์ฌ๋ฏธ์ ์ฑ๋ฆฐ์ง๋ฅผ ๋ ์ด์ฌํ ํ๊ฒ ๋ฉ๋๋ค
2021๋ 07์ 23์ผ ~ 2021๋ 08์ 31์ผ
Front-end: ๊นํํ ์ ๋ฏผ์ฃผ ํธ์์ค
Back-end: ๊น์ ์ฉ ๊น์งํ ๋ฐ์ฐ์ฐ ์ต์๊ท
Dedigner: ์์งํ ์ ์๋น
๐บ ์ ํ๋ธ ๋งํฌ
๐ ๋ฐฑ์๋ Repository
๐ ํ ๋ ธ์
์ฒ์ ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋น์ ์ ์ฒด ์ฑ๋ฆฐ์ง๊ฐ ๋งค์ฐ ์ ์๊ธฐ์
์๋ฒ๋ก๋ถํฐ ๋ชจ๋ ์ฑ๋ฆฐ์ง๋ฅผ ๋ถ๋ฌ์จ ๋ค ํ๋ก ํธ์์ ๋๋ ค์ง ํ๊ทธ์ ๋ง๊ฒ ํํฐ๋ง ํด์ฃผ๋ ํจ์๋ฅผ ๊ตฌํ
// ์๋ฒ๋ก๋ถํฐ ๋ฐ์์ค๋ challenges์ ์ฌ์ฉ์๊ฐ ๋๋ฅธ ํ๊ทธ ๊ฐ filters๋ฅผ ๋น๊ตํ๋ ํจ์
const multiPropsFilter = (challenges, filters) => {
const filterKeys = Object.keys(filters);
return challenges.search.filter((challenge) => {
return filterKeys.every((key) => {
if (!filters[key].length) return true;
if (Array.isArray(challenge[key])) {
return challenge[key].some((keyEle) => filters[key].includes(keyEle));
}
return filters[key].includes(challenge[key]);
});
});
};
const searchProducts = () => {
const filteredProducts = multiPropsFilter(searchList, filteredCollected());
return filteredProducts?.filter((product) => {
return product;
});
};
ํ์ง๋ง ์ ์ ์ฑ๋ฆฐ์ง๊ฐ ๋ง์์ง์๋ก ์๋ฒ๋ก๋ถํฐ ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋๊ฒ ๋นํจ์จ์ ์ด๋ผ๊ณ ํ๋จํ์ ํ๊ทธ๋ฅผ ๋๋ฅธ ๋ค ๊ฒ์ ๋ฒํผ์ ๋๋ฅผ๋ ํด๋น ๋ฐ์ดํฐ๋ฅผ ์๋ฒ๋ก๋ถํฐ ํธ์ถํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝ ์๋
// ์ฌ์ฉ์๊ฐ ๋๋ฅธ ํ๊ทธ ๊ฐ์ collectedTrueKeys์ ๋ด์ ์๋ฒ๋ก ์ ์ก
const collectedTrueKeys = {
categoryName: "",
tags: "",
challengeProgress: "",
};
const { categoryName, tags, progress } = searchState.passingTags;
for (let categoryKey in categoryName) {
if (categoryName[categoryKey])
collectedTrueKeys.categoryName = categoryKey;
}
for (let tagKey in tags) {
if (tags[tagKey]) collectedTrueKeys.tags = tagKey;
}
for (let progressKey in progress) {
if (progress[progressKey]) collectedTrueKeys.progress = progressKey;
}
return collectedTrueKeys;
};
const filter = () => {
dispatch(searchAll.searchFilterDB(filteredCollected()));
};
๋ง๋ค๋ฉด์๋ ํ๊ทธ๋ฅผ ๋๋ฅผ๋ ๋ฐ๋ก ํด๋น ๋ฐ์ดํฐ๋ฅผ ํธ์ถํ๋ ๋ฐฉ์์ด ๋ ํธํ์ง์์๊นํ์๋๋ฐ ์๋๋ ๋ค๋ฅผ๊น ์ ์ ํผ๋๋ฐฑ์ผ๋ก ๋ค์ด์์ ๋ฐ๋ก ์์ ์๋ ๊ธฐ์กด์๋ ๊ฒ์ํ๊ธฐ ๋ฒํผ์ ํตํด api๋ฅผ ํธ์ถํ์ผ๋ ํ๊ทธ์ ์ํ๊ฐ์ ๋ฐ๊พธ๋ ํจ์(allFilterClickListener)์์์ ์คํ์ํค๋ คํ๋ ํ๊ทธ์ ์ํ๊ฐ์ด ๋ณํ๊ธฐ์ ์ api๋ฅผ ํธ์ถํด์ ์คํจ
const allFilterClickListener = (e, filterProp) => {
let name = e.target.textContent;
if (name === "๊ธ์ฐ๊ธ์ฃผ") {
name = "NODRINKNOSMOKE";
} else if (name === "์ด๋") {
name = "EXERCISE";
} else if (name === "์ํ์ฑ๋ฆฐ์ง") {
name = "LIVINGHABITS";
}
...
else {
name = e.target.textContent;
}
setSearchState({
passingTags: {
...searchState.passingTags,
[filterProp]: {
[name]: !searchState.passingTags[filterProp][name],
},
},
});
};
useEffect(() => {
if (keyWord === "ALL") {
dispatch(searchActions.searchFilterDB(filteredCategory(), keyWord));
} else {
return dispatch(
searchActions.searchFilterDB(filteredCategory(), keyWord)
);
}
}, [dispatch, filteredCategory, keyWord, searchState]);
useEffect์ ํ์ฉํ์ฌ ํ๊ทธ๋ฅผ ๋๋ฌ ์ํ๊ฐ์ด ๋ฐ๋๋๋ง๋ค ๋ฐ๋ก api๋ฅผ ํธ์ถ์ํค๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐ
๋ก๊ทธ์ธ์ ๋ฐ์์ค๋ accessToken์ด ๋ง๋ฃ๋์์๋ ๊ฐ์ด ๋ฐ์์จ refreshToken์ ์๋ฒ์ ์ ์กํ๊ณ ์๋ก์ด accessToken๊ณผ refreshToken์ ๊ฐ์ ธ์ ์ฟ ํค์ ์ ์ฅํ๋ ๋ฐฉ์์ ๊ตฌํ
instance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const {
config,
response: { status },
} = error;
if (status === 401) {
if (error.response.data.message === "TokenExpiredError") {
const originalRequest = config;
const refresh_token = getCookie("refreshToken");
const token = getCookie("token");
const { data } = await instance.post(`api/member/reissue`, {
accessToken: token,
refreshToken: refresh_token,
});
const { accessToken, refreshToken } = data;
const accessCookie = { name: "token", value: accessToken };
const refreshCookie = { name: "refreshToken", value: refreshToken };
await multiCookie(accessCookie, refreshCookie);
instance.defaults.headers.common[
"Authorization"
] = `Bearer ${accessToken}`;
originalRequest.headers.common[
"Authorization"
] = `Bearer ${accessToken}`;
return instance(originalRequest);
}
}
}
);
- 401์๋ฌ๋ฅผ interceptor๋ฅผ ํ์ฉํดํ์ฌ ์ฒ๋ฆฌํ์ผ๋ ๊ธฐ์กด์ ์๋ฌ๊ฐ ๋ api์์ฒญ์ ๋ค์ ํ๋ ๋ก์ง์ด ์์๊ณ ,
- ํ ํฐ์ด ๋ง๋ฃ๋ ์ํ๋ก ์ฌ๋ฌ ์ข ๋ฅ์ api๋ฅผ ๋์ ํธ์ถํ ๋ ํธ์ถ๋ ๋ชจ๋ api๋ง๋ค ์์ ๋ก์ง์ด ์คํ๋์ด ํ๋ฒ์ ํ ํฐ ๊ต์ฒด๊ฐ ์ฌ๋ฌ๋ฒ ์ผ์ด๋จ
- ๋ํ ๋ก๊ทธ์ธ์ ๋ฐ์ํ๋ ์ค๋ฅ(์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ ์ค๋ฅ ๋ฑ) ๋ํ 401์๋ฌ์ฌ์ ํด๋น ์ค๋ฅ ๋ฐ์์ ๊ธฐ์กด์ ์ ํด๋ ์๋ฌ์ฒ๋ฆฌ๊ฐ ๋์ํ์ง ์์.
let isTokenRefreshing = false;
let refreshSubscribers = [];
const onTokenRefreshed = (accessToken) => {
refreshSubscribers.map((callback, idx) => {
return callback(accessToken);
});
};
const addRefreshSubscriber = (callback) => {
refreshSubscribers.push(callback);
};
...
instance.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
const originalConfig = err.config;
if (originalConfig.url !== "api/member/login" && err.response) {
if (err.response.status === 401 && !originalConfig._retry) {
originalConfig._retry = true;
if (!isTokenRefreshing) {
isTokenRefreshing = true;
const rs = await refreshTokens();
const { accessToken, refreshToken } = rs.data;
setCookie("token", accessToken);
setCookie("refreshToken", refreshToken);
isTokenRefreshing = false;
instance.defaults.headers.common.Authorization = ` Bearer ${accessToken}`;
originalConfig.headers.Authorization = `Bearer ${accessToken}`;
onTokenRefreshed(accessToken);
refreshSubscribers = [];
return instance(originalConfig);
}
const retryOriginalRequest = new Promise((resolve) => {
addRefreshSubscriber((accessToken) => {
originalConfig.headers.Authorization = "Bearer " + accessToken;
resolve(instance(originalConfig));
});
});
return retryOriginalRequest;
}
if (err.response.status === 403 && err.response.data) {
return Promise.reject(err.response.data);
}
}
return Promise.reject(err);
}
);
1 & 2. ์ฌ๋ฌ ์ข ๋ฅ์ api๋ฅผ ๋์ ํธ์ถํ์ฌ ๋ฐ์ํ ์๋ฌ๋ค์ let refreshSubscribers = []; ์์ ๋ด์๋๊ณ ์ฐจ๋ก๋ก ์คํ์ํด์ผ๋ก ์๋ฌ๊ฐ ๋ api ์์ฒญ์ ํ๋๋ง ์ฒ๋ฆฌ ์คํํ๊ณ accessToken์ ๊ต์ฒดํ ๋ค ๋๋จธ์ง api ์์ฒญ์ ์คํํ๋๊ฒ์ผ๋ก ํด๊ฒฐ 3. ๋ก๊ทธ์ธ์ ๋ฐ์ํ๋ 401 ์๋ฌ๋ if (originalConfig.url !== "api/member/login" && err.response)์ผ๋ก ์์ธ ์ฒ๋ฆฌํจ
//ChatInfinityScroll.js
const _handleScroll = _.throttle(() => {
//๋ก๋ฉ์ค์ด๋ฉด callNext()๋ฅผ ์๋ถ๋ฅด๋๋ก
if (loading) {
return;
}
if (scrollTo.current.scrollTop === 0) {
setPrevHeight(scrollTo.current.scrollHeight);
console.log(scrollTo.current.scrollHeight);
callNext();
}
}, 500);
//MessageList.js
useEffect(() => {
if (prevHeight) {
scrollTo.current.scrollTop = scrollTo.current.scrollHeight - prevHeight;
console.log(prevHeight, scrollTo.current.scrollHeight);
return setPrevHeight(null);
} else {
scrollTo.current.scrollTop =
scrollTo.current.scrollHeight - scrollTo.current.clientHeight;
}
}, [chatInfo.messages]);
-
์ฑํ ๊ธฐ๋ฅ ๊ตฌํ ์ด๋ฐ์ ์ ์ ๊ฐ ๋ฉ์ธ์ง๋ฅผ ์ ๋ ฅํ ๋๋ง๋ค ์คํฌ๋กค์ ์ ์ผ ํ๋จ์ ์์นํ๋๋ก ์ฝ๋ ๊ตฌํ
-
useEffect์ ์์กด ๋ฐฐ์ด์
chatInfo.messages
์ค์ ํด์ InfinityScroll ํตํด ์๋ก์ด ๋ฉ์ธ์ง ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋ ๊ฒฝ์ฐ๋chatInfo.messages
๊ฐ ์ ๋ฐ์ดํธ ๋๋ฏ๋ก useEffect ์คํ๋๋ฉด์ ์คํฌ๋กค ์ ์ผ ํ๋จ์ผ๋ก ์ด๋ -
api ์์ฒญํ๊ธฐ์ ์ ์คํฌ๋กค ์์น๋ฅผ
preHeight
๋ณ์์ ํ ๋น -
preHeight
๊ฐ ์๋ ๊ฒฝ์ฐ ์คํฌ๋กค์ด ํ๋จ์ผ๋ก ๋ด๋ ค๊ฐ์ง๊ณ ์๊ณ ์ ์ฅ๋์ด ์๋ ์์น์ ์๋๋ก useEffect ์ฝ๋ ๋ณ๊ฒฝ