React์ Typescript๋ฅผ ์ด์ฉํ์ฌ ํ๋ก ํธ์๋๋ก๋ง ์นด์นด์คํก ํด๋ก ์ฝ๋ฉํ๊ธฐ
: ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ๋ฆฌ์กํธ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด๊ณ , ์นด์นด์คํก ์ฑํ ๋ฐฉ์ ์ฑํ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณด์!
- textarea ์ํฐ๋ก ๋ฉ์ธ์ง ์ ์ก
- ์คํฌ๋กค ์๋ ๋ด๋ฆฌ๊ธฐ
- ์ ์ ์ ๋ฐ๋ผ ํ๋ฉด ๊ตฌ์ฑ ๋ค๋ฅด๊ฒํ๊ธฐ
์ปดํฌ๋ํธ์ ๋ถ์๊ด๊ณ(?) ๋ ๋ค์๊ณผ ๊ฐ๋ค
- App
- UserList
- UserItem
- ChatList
- ChatItem
- InputForm
// InputForm.tsx
<Wrapper onSubmit={onSubmit}>
<InputField
required
value={value}
onChange={onChange}
onKeyDown={handleEnter}
placeholder="๋ฉ์ธ์ง๋ฅผ ์
๋ ฅํ์ธ์"
/>
<SendButton>์ ์ก</SendButton>
</Wrapper>
์ปดํฌ๋ํธ ๊ตฌ์ฑ์ ์์ ๊ฐ์ด ํ๋ค.
์ธํ ์์ ์๋ ๊ฐ์ value๋ก์จ useState๋ฅผ ์ด์ฉํด ๊ด๋ฆฌํด์ฃผ๊ณ useCallback์ ์ด์ฉํ์ฌ onChange ํจ์๋ฅผ ๋ง๋ค์ด์คฌ๊ณ , ํด๋น ๊ฐ์ ์ ์กํ๋ onSubmit ํจ์๋ฅผ ๋ง๋ค์๋ค.
// App.js
const [value, setValue] = useState("");
const onChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setValue(e.target.value);
}, []);
const onSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
if (value.length == 0) {
return;
}
e?.preventDefault(); // ๋ฒํผ์ ํตํ ์ ์ถ์ด๋ผ๋ฉด ์๋ก๊ณ ์นจ ๋ฐฉ์ง
onConcat(value);
setValue("");
};
Wrapper ์ปดํฌ๋ํธ ์์ฒด๊ฐ >> form << ์ด๊ธฐ ๋๋ฌธ์ inputField ์ required ์์ฑ์ ๋ฃ์ด ๋น ๋ฌธ์์ด์ด ์ ์ก๋๋ ๊ฒ์ ๋ง์๋ค.
ํค๋ณด๋๋ก ์ํฐ๋ฅผ ์น๊ฑฐ๋ ์ ์ก๋ฒํผ์ ๋๋ฌ onSubmit ํจ์๊ฐ ์คํ๋๋๋ฐ, ์ด๋ ์ ์ก๋ฒํผ์ ํด๋ฆญํ๋ ๊ฒ์ form์ ์ํ ์ด๋ฒคํธ์ด๊ธฐ ๋๋ฌธ์ e: React.FormEvent<HTMLFormElement>
๋ก ์ด๋ฒคํธ์ ํ์
์ ์ง์ ํด์ค๋ค.
์ ์(์ํฐ์ณ์ ์ฑํ ์ ์ก)์ ๊ฒฝ์ฐ FORM์ ์ํ ์ด๋ฒคํธ๊ฐ ์๋๊ณ textarea ํน์ฑ์ ๋ฐ๋ก ์ฒ๋ฆฌํด์ฃผ๊ธฐ ๋๋ฌธ์ (์๋ ์ฐธ๊ณ ) form ์ด๋ฒคํธ์ธ e ๋ค์ ? (๋ฌผ์ํ) ๋ฅผ ๋ถ์ฌ ์ต์ ๋ ๋ฐ์ธ๋ฉ ์ฒ๋ฆฌ๋ฅผ ํด์คฌ๋ค.
// App.tsx
const [chats, setChats] = useState<Chat[]>(chatData.chats);
const nextChatId = useRef(chatData.chats.length + 1);
const onConcat = useCallback(
(text: string) => {
const chat = {
id: nextChatId.current,
senderId: curUser,
text,
date: String(new Date()),
};
setChats(chats.concat(chat));
nextChatId.current++;
},
[chats, curUser]
);
onSubmit() ํจ์๊ฐ ์คํ๋๋ฉด ์ธํ์ ์๋ ๊ฐ์ ์ธ์๋ก App.tsx (InputForm.tsx์ ๋ถ๋ชจ ์ปดํฌ๋ํธ) ์ ์ ์ ๋์ด์๋ onConcat() ํจ์๊ฐ ์คํ๋์ด ํด๋น ๊ฐ์ ๋ด์ฉ์ผ๋ก ๊ฐ์ง๋ ๊ฐ์ฒด๊ฐ chats ๋ฐฐ์ด์ ์ถ๊ฐ๋๋ค.
์ฑํ ์ฑ ํน์ฑ์ InputField ์ปดํฌ๋ํธ์์ ์ฌ์ฉ์๊ฐ ์ฌ๋ฌ์ค์ ๊ฐ์ ๋ฐ๋๋ก ํ๊ธฐ ์ํด input ์ด ์๋ textarea ์ธ๊ทธ๋จผํธ๋ฅผ ์ด์ฉํ๋ค.
// InputForm.tsx
const InputField = styled.textarea`
flex: 1;
border: none;
padding: 10px;
word-break: break-all;
`;
ํ ์คํธ๊ฐ ์ฌ๋ฌ ์ค ๋ณด์ด๋๋ก css ์์ฑ ์ง์ ํ๊ธฐ
word-break: break-all
๊ทผ๋ฐ textarea์ ํน์ฑ์ ์ํฐ๋ฅผ ๋๋ฅด๋ฉด onSubmit์ด ์คํ๋์ง ์๊ณ ์ค๋ฐ๊ฟ์ด ๋ฐ์ํ๋ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค.
// ์ํฐ๋ก ์ ์ก, shift+์ํฐ๋ก ์ค๋ฐ๊ฟ
const handleEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
onSubmit();
e.preventDefault();
}
};
์ ์ฝ๋๋ฅผ ์ถ๊ฐํ์ฌ ํค๋ณด๋ ์ด๋ฒคํธ๋ฅผ ๋ฐ๊ณ ์ํฐ๊ฐ ํด๋ฆญ๋ ๋๋ง๋ค onSubmit()์ด ์คํ๋๋๋ก ๋ฐ๊ฟ์ฃผ์๋ค. ๋ํ ์ผ๋ฐ์ ์ธ pc์ฉ ์ฑํ ์ฑ๋ค์ฒ๋ผ ์ํฐ์ ์ฌํํธํค๋ฅผ ํจ๊ป ์น๋ฉด ์ค๋ฐ๊ฟ์ด ๊ทธ๋๋ก ๋ ์ ์๋๋ก ์กฐ๊ฑด์ ์ฒ๋ฆฌํด์ฃผ์๋ค.
์์ธ์ง ์์ด๋ ๋ฌธ์ ๊ฐ ์์๊ธฐ๋๋ฐ ํ๊ธ๋ง ์ ๋ ฅํ๋ฉด ๋๊ธ์๊ฐ ํ๋ฒ์ฉ ๋ ์ ๋ ฅ๋๋ ์ค๋ฅ๊ฐ ์๊ฒผ๋ค.. input์ด ์๋ textarea๋ก ๋ฐ๊พธ๋ ์์๋ ์ค๋ฅ..
return (
<Wrapper onSubmit={onSubmit}>
<InputField
required
value={value}
onChange={onChange}
onKeyPress={handleEnter}
placeholder="๋ฉ์ธ์ง๋ฅผ ์
๋ ฅํ์ธ์"
/>
<SendButton>์ ์ก</SendButton>
</Wrapper>
);
์ฌ๊ธฐ์ onKeyDown
๋์ onKeyPress
๋ก ๋ฐ๊ฟจ๋๋ ๋ฉ๋ํ๊ฒ ํด๊ฒฐ๋์๋ค !
chatList ์ปดํฌ๋ํธ์ overflow: auto;
์์ฑ์ ์ ์ฉํ์ฌ ์ฑํ
์ด ๋ง์์ง๋ฉด ํด๋น ์ปดํฌ๋ํธ์ ์คํฌ๋กค์ด ๋ํ๋๋๋ก ๊ตฌํ์ ํ๋๋ฐ... ์ฑํ
์ ๋ณด๋ผ๋๋ง๋ค ๋ฐฉ๊ธ ๋ณด๋ธ ์ฑํ
์ด ๋ณด์ด๋๋ก ๋งจ ์๋ ์คํฌ๋กค๋ก ์ด๋ํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ณ ์ถ์๋ค.
// ์ฑํ
์ด ์
๋ฐ์ดํธ๋ ๋๋ง๋ค ์๋๋ก ์คํฌ๋กค
const chatListRef = useRef<HTMLDivElement>(null);
useEffect(() => {
chatListRef.current?.scrollTo(0, chatListRef.current.scrollHeight);
console.log("์คํฌ๋กค!");
}, [chats]);
return (
<Wrapper ref={chatListRef}>
...
</Wrapper>
);
๋ณธ ์คํฌ๋กค ํจ์๊ฐ ๋ฐ์ํ ๋ฆฌ์คํธ ์ปดํฌ๋ํธ์ ref ๋ฅผ ์ง์ ํด์ฃผ๋ ๊ฒ์ ์์ง ๋ง์ !!
๊ธฐ๋ณธ์ผ๋ก ์ ํด์ง๋ ์คํฌ๋กค๋ฐ๋ ๋๋ฌด ๋ชป์๊ฒจ์ ๋์์ธ์ ๋ณ๊ฒฝํด์ฃผ์๋ค.
// ChatList.tsx
const Wrapper = styled.div`
display: flex;
flex-direction: column;
padding: 15px;
gap: 20px;
height: 65%;
overflow: auto;
background-color: skyblue;
/* ์คํฌ๋กค๋ฐ ์ปค์คํ
*/
::-webkit-scrollbar {
width: 3px;
}
::-webkit-scrollbar-thumb {
background-color: white;
border-radius: 10px;
}
::-webkit-scrollbar-track {
}
`;
ํค๋๋ถ๋ถ์ ์ ์ ๋ฆฌ์คํธ์์ ์ ์ ๋ฅผ ์ ํํ๋ฉด curUser๊ฐ ์ ํํ ์ ์ ์ id๋ก ๋ณ๊ฒฝ๋๊ณ , ํด๋น ์ ์ ์๊ฒ ๋ง๊ฒ ํ๋ฉด์ด ๊ตฌ์ฑ๋๋ค.
// App.tsx
const changeUser = (id: number) => {
setCurUser(id);
};
// UserItem.tsx
const UserItem = ({ selected, user, changeUser }: UserItemProps) => {
return (
<Wrapper onClick={() => changeUser(user.id)}>
<ProfileImage selected={selected} src={user.profileImage} />
{user.name}
</Wrapper>
);
};
๊ธฐ๋ณธ์ ์ผ๋ก ํ์ฌ ์ ์ ์ ๋ณด, ์ ์ ๊ฐ์ฒด๋ค๊ณผ ์ฑํ ๊ฐ์ฒด๋ค์ useState๋ฅผ ์ด์ฉํ์ฌ ์ํ๋ฅผ ๊ด๋ฆฌํด์ค๋ค
const [curUser, setCurUser] = useState(1);
const [users, setUsers] = useState<User[]>(userData.users);
const [chats, setChats] = useState<Chat[]>(chatData.chats);
์ฑํ ๊ฐ์ฒด๋ ๊ฐ ์ฑํ ์ ์์ด๋, ์ ์กํ ์ ์ ์ ์์ด๋, ํ ์คํธ, ๋ณด๋ด์ง ๋ ์ง๋ค์ ๋ํ ์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ค
// interfact.ts
export interface Chat {
id: number;
senderId: number;
text: string;
date: string;
}
// chatData.json
{
"chats" : [
{ "id": 1, "senderId": 0, "text": "์ ๋ง", "date": "Sat Oct 01 2022 02:49:31 GMT+0900" },
{ "id": 2, "senderId": 0, "text": "๊ณ ๋ง์์", "date": "Sat Oct 01 2022 02:49:31 GMT+0900" },
{ "id": 3, "senderId": 1, "text": "์์์ค์", "date": "Sat Oct 01 2022 02:49:31 GMT+0900" },
{ "id": 4, "senderId": 2, "text": "์ข์ํด์", "date": "Sat Oct 01 2022 02:49:31 GMT+0900" }
]
}
ํ์ฌ์ ์ ์ ๋ณด (curUser)๋ ์์ ์ปดํฌ๋ํธ๋ค์๊ฒ ์ ๋ฌ๋์ด ๋ง๋จ์ ์๋ ChatItem ์ปดํฌ๋ํธ์์ ํด๋น ์ ๋ณด๋ฅผ ์ด์ฉํ๋ค
// ChatItem.tsx
<Wrapper isCurUser={isCurUser}>
{isCurUser ? (
<>
<ChatWrapper>
{time}:{minute}
<ChatBalloon isCurUser={true}>{chat.text}</ChatBalloon>
</ChatWrapper>
</>
) : (
<>
<ProfileImage src={sender.profileImage} />
<ContentWrapper>
{sender.name}
<ChatWrapper>
<ChatBalloon isCurUser={false}>{chat.text}</ChatBalloon>
{time}:{minute}
</ChatWrapper>
</ContentWrapper>
</>
)}
</Wrapper>
ChatItem ์ปดํฌ๋ํธ์ ๊ตฌ์ฑ
์ ๋ฌ๋ฐ์ ํ์ฌ ์ ์ ์ ์ ๋ณด๋ฅผ ํ๋กํผํฐ๋ก ๋ฐ์ ChatBalloon์ ์คํ์ผ์ด ๋ฐ๋๋ค
npm i --save-dev @types/styled-components
const Wrapper = styled.div`
display: flex;
gap: 10px;
justify-content: ${({ isCurUser }: { isCurUser: boolean }) =>
isCurUser ? "flex-end" : "flex-start"};
width: 100%;
`;
const ChatBalloon = styled.div`
background-color: ${({ isCurUser }: { isCurUser: boolean }) =>
isCurUser ? "yellow" : "white"};
padding: 10px;
border-radius: 5px;
word-break: break-all;
`;
jsx์์ ์ฌ์ฉํ๋๊ฒ๊ณผ ๊ฑฐ์ ์ ์ฌํ๋ฐ, ํ๋กํผํฐ์ ํ์ ์ ์ง์ ํ๋๊ฑธ ์์ง๋ง์!
โโโ README.md
โโโ package-lock.json
โโโ package.json
โโโ public
โ โโโ favicon.ico
โ โโโ index.html
โ โโโ logo192.png
โ โโโ logo512.png
โ โโโ manifest.json
โ โโโ robots.txt
โโโ src
โ โโโ App.tsx
โ โโโ StyledComponents.tsx
โ โโโ components
โ โ โโโ ChatItem.tsx
โ โ โโโ ChatList.tsx
โ โ โโโ InputForm.tsx
โ โ โโโ UserItem.tsx
โ โ โโโ UserList.tsx
โ โโโ custom.d.ts
โ โโโ data
โ โ โโโ chatData.json
โ โ โโโ userData.json
โ โโโ index.tsx
โ โโโ interface.tsx
โ โโโ profileAssets
โ โโโ dedenne.jpeg
โ โโโ morpeco.png
โ โโโ tmp.jpeg
โโโ tsconfig.json
๋์ค์.. ๊ณง... ์ธ์ ๊ฐ... ๊ตฌํํ .. ์ถ๊ฐ์ ์ธ ๋ด์ฉ๋ค...
์ ์ ๋ชฉ๋ก์์ ์ ์ ๋ฅผ ์ ํํ์ ๋ ์์์ฒ๋ผ ํ๋กํ ์ด๋ฏธ์ง ์์ ๋ฐํฌ๋ช
ํ ๋ ์ด์์๊ณผ ์ ํ์ค ์ด๋ผ๋ ๊ธ์๊ฐ ๋จ๊ฒ ๊ตฌํํ๊ธฐ
(ํ์ฌ ์์๋ฐฉํธ์ผ๋ก ์ ํ์ค์ธ ํ๋กํ์ด๋ฏธ์ง์ ํฌ๋ช
๋๋ง ๋ฎ์์ง๊ฒ ๊ตฌํ)
ChatItem ๋ ์ด์์์ ๊ตฌ์ฑํ ๋ ๋ ํจ์จ์ ์ด๊ฒ ๊ตฌ์ฑํ ์ ์๋ ๋ฐฉ๋ฒ๊ณผ ์์ ๋ณ์๋ช
์๋ช
์ ์ฐพ๋์ค์
๋๋ค
ํ ์ ์ ๊ฐ ์ฐ์์ผ๋ก ๋ฉ์ธ์ง๋ฅผ ๋ณด๋์ ๋ ํ๋กํ๊ณผ ์ด๋ฆ ์์ด ๋งํ์ ๋ง ์ญ ๋์ค๊ฒ ํ๋ ๊ธฐ๋ฅ ๊ตฌํ ์์ (์ธ์ ๊ฐ...) -> chat ๊ฐ์ฒด์์ ์ง์ ์ ๋ณด๋ด์ง ์ฑํ
์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ฉด ๊ตฌํ ๊ฐ๋ฅํ๋ค๊ณ ํ๋ค !
์์ธ์ง App.tsx ์์ wrapper์ ๋ถ์ฌํ border ์์ฑ์ด ์๋จนํ๋ค.. -> z-index ์์ฑ์ ์ด์ฉํด๋ณด์
(์ ํํ๊ฒ๋ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ค์ ๋ฐฐ๊ฒฝ์์ ๋ฎ์ฌ๋ฒ๋ ค์ border radius๋ฅผ ์ ์ฉํ border๊ฐ ๋ณด์ด์ง ์์์)
ํ๊ธ๋ง ์
๋ ฅํ๋ฉด ๋ ๊ธ์๊ฐ ํ๋ฒ ๋ ๋ณด๋ด์ง๋ ์ค๋ฅ๊ฐ ์๋๋ฐ, ์์ด์ผ๋๋ ๋ ์๋์ค๋ ์ค๋ฅ๊ฐ.. -> ์์ ์๋ฃ !! (feat. @yjoonjang)
input์ ์๋ ๊ฐ์ ์ฌ๋ถ์ ๋ฐ๋ผ ์ ์ก๋ฒํผ ํ์ฑํ
์๊ฐ ๋ ์ ํํ๊ฒ ํ์ํ๊ธฐ