์ค๊ฐ๊ณ ์ฌ ๊ธฐ๊ฐ ์ ๋ณด๋ด์
จ๋์.. ์ ๋ชป๋ณด๋์ต๋๋ค. ํํ
๋ค์ ํ๊ธฐ์ ๋ฌด์กฐ๊ฑด ๊ธฐ์์ฌ์ ๋ค์ด๊ฐ๊ฒ ๋ค๋ ์๊ฐ์ ๋ค์ ํ๋ฒ ํด๋ณด๋ฉด์, ์ด๋ฒ์ฃผ WIL์ ์ ๊ณ ์์ต๋๋ค.
์ค๊ฐ ํด์๊ธฐ๊ฐ๋์ ์ง์์ง์ํ๊ฒ ํ๋ก์ ํธ๋ฅผ ๊ฑด๋๋ ค๋ณด์์ต๋๋ค.
1. ์ด์ ์ ์์๋, ๋ฐํ ์ํธ ๊ด๋ จ ๋ฌธ์
๊ฐ TodoItem๋ง๋ค ์๋ ๋ฐํ ์ํธ๋ฅผ ๋ ์์๋ก ์ฌ๋ ค์ ๊ด๋ฆฌํ๋๋ก ํ๋ค. ์ด์ ์ด๋ค ํฌ๋์์ ์ฐ ๋ฐํ ์ํธ์ธ์ง ๋ฐ๋ก ์ ์ ์๊ธฐ ๋๋ฌธ์, ์ ํ๋ ํฌ๋๋ฅผ ์ ์ญ์ํ๋ก ๊ด๋ฆฌํ๋๋ก ํ๋ค.
useBottomSheet.tsx
import { useRecoilState } from 'recoil';
import { ITodoItem } from '../interfaces/ITodoItem';
import { bottomSheetState } from '../stores/bottomSheet';
function useBottomSheet(initial: boolean) {
const [bottomSheet, setBottomSheet] = useRecoilState(bottomSheetState);
const { isOpen, selectedItem } = bottomSheet;
function onOpen(item: ITodoItem) {
setBottomSheet({ selectedItem: item, isOpen: true });
}
function onDismiss() {
setBottomSheet({ selectedItem: null, isOpen: false });
}
return { isOpen, selectedItem, onOpen, onDismiss };
}
export default useBottomSheet;
์ด๋ฆผ/๋ซํ ์ํ์ ์ ํ๋ ํฌ๋๋ฅผ ๋ฆฌ์ฝ์ผ atom์ผ๋ก ๋ง๋ค์ด ๊ด๋ฆฌํ๋ค. ๊ฐ๋จ๊ฐ๋จ์ฐ.
2. Todo ๊ด๋ จ ๋ก์ง ์ถ์ํ
UI์ปดํฌ๋ํธ์์ ์ต๋ํ ๋น์ฆ๋์ค ๋ก์ง์ ๋ถ๋ฆฌํ๊ณ , ์ปค์คํ
ํ
์ ์ด์ฉํด ๋๊ฒจ๋ฐ์ ์ฌ์ฉํ๋ค.
์ ๋ฒ์ฃผ ์คํฐ๋ ์๊ฐ์ ๋ณด์ฌ๋๋ ธ๋ ๋ด์ฉ์ด์ฃ ??
const useTodo = () => {
const [todo, setTodo] = useRecoilState(todoState);
const insertTodo = (inputValue: string, category: ICategory) => {
if (inputValue) {
const newTodo = {
label: inputValue,
id: uuid(),
isDone: false,
category: category,
};
setTodo((prev) => [...prev, newTodo]);
}
};
const editTodo = (inputValue: string, id: string) => {
if (inputValue) {
const index = todo.findIndex((v) => v.id === id);
const temp = [...todo];
temp[index] = { ...temp[index], label: inputValue };
setTodo(temp);
}
};
const toggleTodo = (id: string) => {
const index = todo.findIndex((v) => v.id === id);
const temp = [...todo];
temp[index] = { ...temp[index], isDone: !temp[index].isDone };
setTodo(temp);
};
const deleteTodo = (id: string) => {
setTodo(todo.filter((v) => v.id !== id));
};
return { insertTodo, editTodo, toggleTodo, deleteTodo };
};
export default useTodo;
ํฌ๋ ์ญ์ ํ๊ธฐ ๋ฑ์ ํจ์๋ฅผ ์ปค์คํ
ํ
์ ํตํด ๋ฐ์์ฌ ์ ์๊ณ , ํฌ๋ ์ํ(์ญ์ ํ ํฌ๋์ id) ๋ํ ์ ์ญ์ผ๋ก ๊ด๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ํ๋ก์ ํธ ์ด๋์์๋ผ๋ ์ฌ์ฉํ ์ ์๋ค. ํฌ๋ ์ปดํฌ๋ํธ์ ๋จ์ด์ ธ์๋ ๋ฐํ
์ํธ ์ปดํฌ๋ํธ์์ ๋ฐ์์ ์ฌ์ฉํ๊ธฐ์ ์ข๋ค.
3. ์น๊ตฌ ๋ชฉ๋ก (๋ฌธ์์ด์์ ์ด๋ชจ์ง ๋ค๋ฃจ๊ธฐ)

๋๋ฆ ๊ณ ๋ฏผ์ ๋ง์ด ํ๋ค.
const initialState: IFriend[] = [
{
userId: 'user1',
name: '๐ฌ๊ท์ง',
profileImage: '',
statusMessage: '์ด์ฉ๋ค ๊ฐ์',
},
{
userId: 'user2',
name: '์ ์ง',
profileImage: 'https://i.ibb.co/MgmDcz1/1021805078815985664.webp',
statusMessage: '์ฃผ๋์ด PM์ด ๋๊ธฐ ์ํ ๋
ธ๋ ฅ',
},
{
userId: 'user3',
name: '๊นํ
์คํธ',
profileImage: '',
statusMessage: 'user3',
}
];
์น๊ตฌ ๊ฐ์ฒด๋ ์ด๋ฐ ํ์์ด๋ค. ํ๋กํ ์ด๋ฏธ์ง ๋ถ๋ถ์์ ์๊ฐ์ ๋ง์ด ์ผ๋ค.
ํ๋กํ์ด๋ฏธ์ง๊ฐ ์์ ๋๋ ํด๋น ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๋ณด์ฌ์ฃผ๊ณ , ์์๋๋ ์ด๋ฆ์ ์ฒซ๊ธ์๋ฅผ ๋ณด์ฌ์ค๋ค.
FriendsIcon.tsx
const ProfileImage = styled.div<{ friend: IFriend; selected: boolean }>`
position: relative;
width: 40px;
height: 40px;
border-radius: 50px;
background-color: ${({ theme }) => theme.palette.mono.gray_f5};
${({ friend }) => getImageStyle(friend)}
border: ${({ selected, theme }) =>
selected && `2px solid ${theme.palette.mono.gray_44}`}
`;
const getImageStyle = (friend: IFriend) => {
switch (friend.profileImage.length) {
case 0:
return css`
::after {
content: '${sliceFirstWord(friend.name)}';
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
font-weight: 500;
}
`;
default:
return css`
background-image: url(${friend.profileImage});
background-size: 40px 40px;
background-position: center;
`;
}
};
ํ๋กํ ์ด๋ฏธ์ง์ ๊ดํ ๋ถ๋ถ๋ง ๋ฐ๋ก ๋นผ์ ๋ฃ์ด์ฃผ์๋ค. ์ด๋ฆ์ ์ฒซ๊ธ์๋ฅผ ๋ณด์ฌ์ค ๋๋ after ๊ฐ์ ์ ํ์๋ฅผ ์ด์ฉํ๋ค.

์ด ๊ณผ์ ์์ ๋ฌธ์ ๊ฐ ํ๋ ์์๋ค. ํฌ๋๋ฉ์ดํธ์ ์ด๋ฆ์ ์ฒซ๊ธ์๋ก ์ด๋ชจ์ง๋ฅผ ์ฐ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ, ํ๋กํ ์ด๋ฏธ์ง์ ์ด๋ชจ์ง๊ฐ ๋ค์ด๊ฐ๋ฉด ๊ฝค ์ด์๋ค. ๋ฌด์์ ๋ฌธ์์ด์ ์ฒซ๋ฒ์งธ ์ธ๋ฑ์ค๋ฅผ ๊ฐ์ ธ์ค๋๊น ์ด๋ชจ์ง๊ฐ ๊นจ์ก๋ค. ์ด๋ชจ์ง๋ ์ ๋์ฝ๋๋ฅผ ์ฐ๋๋ฐ, ๋ฌธ์์ด๋ก ์ฒ๋ฆฌ๋ ๋ ๊ธธ์ด๊ฐ ๋ง 2,3 ์ด๋ ๊ฒ ๋์ค๋๋ผ.
sliceFirstWord
const sliceFirstWord = (string: string) => {
// https://avengersrhydon1121.tistory.com/268
if (
/^([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/.test(
string,
)
) {
return string.slice(0, 2);
} else {
return string[0];
}
};
๋ฐ์ ๋ฌธ์์ด์ด ์ด๋ชจ์ง๋ก ์์ํ๋ฉด 0,1 ์ธ๋ฑ์ค๋ฅผ ๋ฆฌํดํ๊ณ , ์๋๋ฉด 0๋ฒ์งธ ์ธ๋ฑ์ค๋ง ๋ฆฌํดํ๋๋ก ํ๋ค. ์ ๊ท์์ ์ฌ์ฉํ๋๋ฐ, /^ / ๋ฅผ ํตํด ์ด๋ ํ ๋ฌธ์๋ก ์์ํ๋์ง ์ ์ ์๋ค.

4. ๋ ๋๋ง ์ต์ ํ?
์ค๊ฐ๊ณ ์ฌ ๊ธฐ๊ฐ ์ ๋ณด๋ด์
จ๋์.. ์ ๋ชป๋ณด๋์ต๋๋ค. ํํ
๋ค์ ํ๊ธฐ์ ๋ฌด์กฐ๊ฑด ๊ธฐ์์ฌ์ ๋ค์ด๊ฐ๊ฒ ๋ค๋ ์๊ฐ์ ๋ค์ ํ๋ฒ ํด๋ณด๋ฉด์, ์ด๋ฒ์ฃผ WIL์ ์ ๊ณ ์์ต๋๋ค.
์ค๊ฐ ํด์๊ธฐ๊ฐ๋์ ์ง์์ง์ํ๊ฒ ํ๋ก์ ํธ๋ฅผ ๊ฑด๋๋ ค๋ณด์์ต๋๋ค.
1. ์ด์ ์ ์์๋, ๋ฐํ ์ํธ ๊ด๋ จ ๋ฌธ์
๊ฐ TodoItem๋ง๋ค ์๋ ๋ฐํ ์ํธ๋ฅผ ๋ ์์๋ก ์ฌ๋ ค์ ๊ด๋ฆฌํ๋๋ก ํ๋ค. ์ด์ ์ด๋ค ํฌ๋์์ ์ฐ ๋ฐํ ์ํธ์ธ์ง ๋ฐ๋ก ์ ์ ์๊ธฐ ๋๋ฌธ์, ์ ํ๋ ํฌ๋๋ฅผ ์ ์ญ์ํ๋ก ๊ด๋ฆฌํ๋๋ก ํ๋ค.
useBottomSheet.tsx
import { useRecoilState } from 'recoil';
import { ITodoItem } from '../interfaces/ITodoItem';
import { bottomSheetState } from '../stores/bottomSheet';
function useBottomSheet(initial: boolean) {
const [bottomSheet, setBottomSheet] = useRecoilState(bottomSheetState);
const { isOpen, selectedItem } = bottomSheet;
function onOpen(item: ITodoItem) {
setBottomSheet({ selectedItem: item, isOpen: true });
}
function onDismiss() {
setBottomSheet({ selectedItem: null, isOpen: false });
}
return { isOpen, selectedItem, onOpen, onDismiss };
}
export default useBottomSheet;
์ด๋ฆผ/๋ซํ ์ํ์ ์ ํ๋ ํฌ๋๋ฅผ ๋ฆฌ์ฝ์ผ atom์ผ๋ก ๋ง๋ค์ด ๊ด๋ฆฌํ๋ค. ๊ฐ๋จ๊ฐ๋จ์ฐ.
2. Todo ๊ด๋ จ ๋ก์ง ์ถ์ํ
UI์ปดํฌ๋ํธ์์ ์ต๋ํ ๋น์ฆ๋์ค ๋ก์ง์ ๋ถ๋ฆฌํ๊ณ , ์ปค์คํ
ํ
์ ์ด์ฉํด ๋๊ฒจ๋ฐ์ ์ฌ์ฉํ๋ค.
์ ๋ฒ์ฃผ ์คํฐ๋ ์๊ฐ์ ๋ณด์ฌ๋๋ ธ๋ ๋ด์ฉ์ด์ฃ ??
const useTodo = () => {
const [todo, setTodo] = useRecoilState(todoState);
const insertTodo = (inputValue: string, category: ICategory) => {
if (inputValue) {
const newTodo = {
label: inputValue,
id: uuid(),
isDone: false,
category: category,
};
setTodo((prev) => [...prev, newTodo]);
}
};
const editTodo = (inputValue: string, id: string) => {
if (inputValue) {
const index = todo.findIndex((v) => v.id === id);
const temp = [...todo];
temp[index] = { ...temp[index], label: inputValue };
setTodo(temp);
}
};
const toggleTodo = (id: string) => {
const index = todo.findIndex((v) => v.id === id);
const temp = [...todo];
temp[index] = { ...temp[index], isDone: !temp[index].isDone };
setTodo(temp);
};
const deleteTodo = (id: string) => {
setTodo(todo.filter((v) => v.id !== id));
};
return { insertTodo, editTodo, toggleTodo, deleteTodo };
};
export default useTodo;
ํฌ๋ ์ญ์ ํ๊ธฐ ๋ฑ์ ํจ์๋ฅผ ์ปค์คํ
ํ
์ ํตํด ๋ฐ์์ฌ ์ ์๊ณ , ํฌ๋ ์ํ(์ญ์ ํ ํฌ๋์ id) ๋ํ ์ ์ญ์ผ๋ก ๊ด๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ํ๋ก์ ํธ ์ด๋์์๋ผ๋ ์ฌ์ฉํ ์ ์๋ค. ํฌ๋ ์ปดํฌ๋ํธ์ ๋จ์ด์ ธ์๋ ๋ฐํ
์ํธ ์ปดํฌ๋ํธ์์ ๋ฐ์์ ์ฌ์ฉํ๊ธฐ์ ์ข๋ค.
3. ์น๊ตฌ ๋ชฉ๋ก (๋ฌธ์์ด์์ ์ด๋ชจ์ง ๋ค๋ฃจ๊ธฐ)

๋๋ฆ ๊ณ ๋ฏผ์ ๋ง์ด ํ๋ค.
const initialState: IFriend[] = [
{
userId: 'user1',
name: '๐ฌ๊ท์ง',
profileImage: '',
statusMessage: '์ด์ฉ๋ค ๊ฐ์',
},
{
userId: 'user2',
name: '์ ์ง',
profileImage: 'https://i.ibb.co/MgmDcz1/1021805078815985664.webp',
statusMessage: '์ฃผ๋์ด PM์ด ๋๊ธฐ ์ํ ๋
ธ๋ ฅ',
},
{
userId: 'user3',
name: '๊นํ
์คํธ',
profileImage: '',
statusMessage: 'user3',
}
];
์น๊ตฌ ๊ฐ์ฒด๋ ์ด๋ฐ ํ์์ด๋ค. ํ๋กํ ์ด๋ฏธ์ง ๋ถ๋ถ์์ ์๊ฐ์ ๋ง์ด ์ผ๋ค.
ํ๋กํ์ด๋ฏธ์ง๊ฐ ์์ ๋๋ ํด๋น ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๋ณด์ฌ์ฃผ๊ณ , ์์๋๋ ์ด๋ฆ์ ์ฒซ๊ธ์๋ฅผ ๋ณด์ฌ์ค๋ค.
FriendsIcon.tsx
const ProfileImage = styled.div<{ friend: IFriend; selected: boolean }>`
position: relative;
width: 40px;
height: 40px;
border-radius: 50px;
background-color: ${({ theme }) => theme.palette.mono.gray_f5};
${({ friend }) => getImageStyle(friend)}
border: ${({ selected, theme }) =>
selected && `2px solid ${theme.palette.mono.gray_44}`}
`;
const getImageStyle = (friend: IFriend) => {
switch (friend.profileImage.length) {
case 0:
return css`
::after {
content: '${sliceFirstWord(friend.name)}';
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
font-weight: 500;
}
`;
default:
return css`
background-image: url(${friend.profileImage});
background-size: 40px 40px;
background-position: center;
`;
}
};
ํ๋กํ ์ด๋ฏธ์ง์ ๊ดํ ๋ถ๋ถ๋ง ๋ฐ๋ก ๋นผ์ ๋ฃ์ด์ฃผ์๋ค. ์ด๋ฆ์ ์ฒซ๊ธ์๋ฅผ ๋ณด์ฌ์ค ๋๋ after ๊ฐ์ ์ ํ์๋ฅผ ์ด์ฉํ๋ค.

์ด ๊ณผ์ ์์ ๋ฌธ์ ๊ฐ ํ๋ ์์๋ค. ํฌ๋๋ฉ์ดํธ์ ์ด๋ฆ์ ์ฒซ๊ธ์๋ก ์ด๋ชจ์ง๋ฅผ ์ฐ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ, ํ๋กํ ์ด๋ฏธ์ง์ ์ด๋ชจ์ง๊ฐ ๋ค์ด๊ฐ๋ฉด ๊ฝค ์ด์๋ค. ๋ฌด์์ ๋ฌธ์์ด์ ์ฒซ๋ฒ์งธ ์ธ๋ฑ์ค๋ฅผ ๊ฐ์ ธ์ค๋๊น ์ด๋ชจ์ง๊ฐ ๊นจ์ก๋ค. ์ด๋ชจ์ง๋ ์ ๋์ฝ๋๋ฅผ ์ฐ๋๋ฐ, ๋ฌธ์์ด๋ก ์ฒ๋ฆฌ๋ ๋ ๊ธธ์ด๊ฐ ๋ง 2,3 ์ด๋ ๊ฒ ๋์ค๋๋ผ.
sliceFirstWord
const sliceFirstWord = (string: string) => {
// https://avengersrhydon1121.tistory.com/268
if (
/^([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/.test(
string,
)
) {
return string.slice(0, 2);
} else {
return string[0];
}
};
๋ฐ์ ๋ฌธ์์ด์ด ์ด๋ชจ์ง๋ก ์์ํ๋ฉด 0,1 ์ธ๋ฑ์ค๋ฅผ ๋ฆฌํดํ๊ณ , ์๋๋ฉด 0๋ฒ์งธ ์ธ๋ฑ์ค๋ง ๋ฆฌํดํ๋๋ก ํ๋ค. ์ ๊ท์์ ์ฌ์ฉํ๋๋ฐ, /^ / ๋ฅผ ํตํด ์ด๋ ํ ๋ฌธ์๋ก ์์ํ๋์ง ์ ์ ์๋ค.
