๐Ÿ’ฌ React์™€ TS๋กœ ์นด์นด์˜คํ†ก ํด๋ก ์ฝ”๋”ฉ ๐Ÿ’›

SeonDalยท2022๋…„ 10์›” 2์ผ
8

ย ๐ŸŒฟ ํšŒ๊ณ ๋ก

๋ชฉ๋ก ๋ณด๊ธฐ
3/7
post-thumbnail
post-custom-banner

React์™€ Typescript๋ฅผ ์ด์šฉํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ๋กœ๋งŒ ์นด์นด์˜คํ†ก ํด๋ก ์ฝ”๋”ฉํ•˜๊ธฐ
: ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ฆฌ์•กํŠธ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด๊ณ , ์นด์นด์˜คํ†ก ์ฑ„ํŒ…๋ฐฉ์˜ ์ฑ„ํŒ… ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž!

  • textarea ์—”ํ„ฐ๋กœ ๋ฉ”์„ธ์ง€ ์ „์†ก
  • ์Šคํฌ๋กค ์ž๋™ ๋‚ด๋ฆฌ๊ธฐ
  • ์œ ์ €์— ๋”ฐ๋ผ ํ™”๋ฉด ๊ตฌ์„ฑ ๋‹ค๋ฅด๊ฒŒํ•˜๊ธฐ

๊ฒฐ๊ณผ๋ฌผ
https://react-messanger-16th-ebon.vercel.app/



๐Ÿงฉ Components

์ปดํฌ๋„ŒํŠธ์˜ ๋ถ€์ž๊ด€๊ณ„(?) ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค

  • App
    • UserList
      • UserItem
    • ChatList
      • ChatItem
    • InputForm


โœจ Main Feat

1. ๋ฉ”์„ธ์ง€ ๋ณด๋‚ด๊ธฐ

// 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 ๋ฐฐ์—ด์— ์ถ”๊ฐ€๋œ๋‹ค.


textarea ์—”ํ„ฐ๋กœ ์ „์†ก (shift&enter = ์ค„๋ฐ”๊ฟˆ)

์ฑ„ํŒ… ์•ฑ ํŠน์„ฑ์ƒ 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์šฉ ์ฑ„ํŒ… ์•ฑ๋“ค์ฒ˜๋Ÿผ ์—”ํ„ฐ์™€ ์‰ฌํ”„ํŠธํ‚ค๋ฅผ ํ•จ๊ป˜ ์น˜๋ฉด ์ค„๋ฐ”๊ฟˆ์ด ๊ทธ๋Œ€๋กœ ๋  ์ˆ˜ ์žˆ๋„๋ก ์กฐ๊ฑด์„ ์ฒ˜๋ฆฌํ•ด์ฃผ์—ˆ๋‹ค.


ํ•œ๊ธ€ ์ค‘๋ณต ์ž…๋ ฅ ์˜ค๋ฅ˜ (onKeyDown)

์™œ์ธ์ง€ ์˜์–ด๋Š” ๋ฌธ์ œ๊ฐ€ ์•ˆ์ƒ๊ธฐ๋Š”๋ฐ ํ•œ๊ธ€๋งŒ ์ž…๋ ฅํ•˜๋ฉด ๋๊ธ€์ž๊ฐ€ ํ•œ๋ฒˆ์”ฉ ๋” ์ž…๋ ฅ๋˜๋Š” ์˜ค๋ฅ˜๊ฐ€ ์ƒ๊ฒผ๋‹ค.. input์ด ์•„๋‹Œ textarea๋กœ ๋ฐ”๊พธ๋‹ˆ ์‹œ์ž‘๋œ ์˜ค๋ฅ˜..

return (
    <Wrapper onSubmit={onSubmit}>
      <InputField
        required
        value={value}
        onChange={onChange}
        onKeyPress={handleEnter}
        placeholder="๋ฉ”์„ธ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
      />
      <SendButton>์ „์†ก</SendButton>
    </Wrapper>
  );

์—ฌ๊ธฐ์„œ onKeyDown ๋Œ€์‹  onKeyPress ๋กœ ๋ฐ”๊ฟจ๋”๋‹ˆ ๋ฉ€๋”ํ•˜๊ฒŒ ํ•ด๊ฒฐ๋˜์—ˆ๋‹ค !


์ฑ„ํŒ… ๋ณด๋‚ผ๋•Œ๋งˆ๋‹ค ์Šคํฌ๋กค ์ž๋™ ๋‚ด๋ฆฌ๊ธฐ (feat. useRef)

chatList ์ปดํฌ๋„ŒํŠธ์— overflow: auto; ์†์„ฑ์„ ์ ์šฉํ•˜์—ฌ ์ฑ„ํŒ…์ด ๋งŽ์•„์ง€๋ฉด ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์— ์Šคํฌ๋กค์ด ๋‚˜ํƒ€๋‚˜๋„๋ก ๊ตฌํ˜„์€ ํ–ˆ๋Š”๋ฐ... ์ฑ„ํŒ…์„ ๋ณด๋‚ผ๋•Œ๋งˆ๋‹ค ๋ฐฉ๊ธˆ ๋ณด๋‚ธ ์ฑ„ํŒ…์ด ๋ณด์ด๋„๋ก ๋งจ ์•„๋ž˜ ์Šคํฌ๋กค๋กœ ์ด๋™ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.

  // ์ฑ„ํŒ…์ด ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋งˆ๋‹ค ์•„๋ž˜๋กœ ์Šคํฌ๋กค
  const chatListRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    chatListRef.current?.scrollTo(0, chatListRef.current.scrollHeight);
    console.log("์Šคํฌ๋กค!");
  }, [chats]);
  • ๊ตฌ๊ธ€๋ง์„ ํ†ตํ•ด scrollTo ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์ฐพ์•„๋ƒˆ๋‹ค.
  • useRef๋กœ ํ•ด๋‹น ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€๋‹ค.
  • useEffect ๋ฅผ ์ด์šฉํ•˜์—ฌ 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 {
  }
`;

2. ์„ ํƒํ•œ ์œ ์ €์— ๋”ฐ๋ผ ํ™”๋ฉด ๊ตฌ์„ฑ ๋‹ฌ๋ผ์ง€๊ฒŒ ๋ถ„๊ธฐ์ฒ˜๋ฆฌ

ํ—ค๋”๋ถ€๋ถ„์˜ ์œ ์ €๋ฆฌ์ŠคํŠธ์—์„œ ์œ ์ €๋ฅผ ์„ ํƒํ•˜๋ฉด 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)

ํ˜„์žฌ์œ ์ €์ •๋ณด (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 ์ปดํฌ๋„ŒํŠธ์˜ ๊ตฌ์„ฑ


tsx์—์„œ styled-components ์กฐ๊ฑด๋ณ„ ์Šคํƒ€์ผ๋ง

์ „๋‹ฌ๋ฐ›์€ ํ˜„์žฌ ์œ ์ €์˜ ์ •๋ณด๋ฅผ ํ”„๋กœํผํ‹ฐ๋กœ ๋ฐ›์•„ ChatBalloon์˜ ์Šคํƒ€์ผ์ด ๋ฐ”๋€๋‹ค

(1) tsx์—์„œ ์Šคํƒ€์ผ๋“œ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

npm i --save-dev @types/styled-components

(2) ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์—์„œ 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์—์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ๊ณผ ๊ฑฐ์˜ ์œ ์‚ฌํ•œ๋ฐ, ํ”„๋กœํผํ‹ฐ์— ํƒ€์ž…์„ ์ง€์ •ํ•˜๋Š”๊ฑธ ์žŠ์ง€๋ง์ž!



๐Ÿ“‚ย Foldering

โ”œโ”€โ”€ 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


๐Ÿ”ฎ Later..

๋‚˜์ค‘์—.. ๊ณง... ์–ธ์  ๊ฐ€... ๊ตฌํ˜„ํ• .. ์ถ”๊ฐ€์ ์ธ ๋‚ด์šฉ๋“ค...

  • ์œ ์ €๋ชฉ๋ก์—์„œ ์œ ์ €๋ฅผ ์„ ํƒํ–ˆ์„ ๋•Œ ์˜ˆ์‹œ์ฒ˜๋Ÿผ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์œ„์— ๋ฐ˜ํˆฌ๋ช…ํ•œ ๋ ˆ์ด์•„์›ƒ๊ณผ ์„ ํƒ์ค‘ ์ด๋ผ๋Š” ๊ธ€์ž๊ฐ€ ๋œจ๊ฒŒ ๊ตฌํ˜„ํ•˜๊ธฐ
    (ํ˜„์žฌ ์ž„์‹œ๋ฐฉํŽธ์œผ๋กœ ์„ ํƒ์ค‘์ธ ํ”„๋กœํ•„์ด๋ฏธ์ง€์˜ ํˆฌ๋ช…๋„๋งŒ ๋‚ฎ์•„์ง€๊ฒŒ ๊ตฌํ˜„)

  • ChatItem ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์„ฑํ• ๋•Œ ๋” ํšจ์œจ์ ์ด๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•๊ณผ ์˜ˆ์œ ๋ณ€์ˆ˜๋ช… ์ž‘๋ช…์„ ์ฐพ๋Š”์ค‘์ž…๋‹ˆ๋‹ค

  • ํ•œ ์œ ์ €๊ฐ€ ์—ฐ์†์œผ๋กœ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋ƒˆ์„ ๋•Œ ํ”„๋กœํ•„๊ณผ ์ด๋ฆ„ ์—†์ด ๋งํ’์„ ๋งŒ ์ญ‰ ๋‚˜์˜ค๊ฒŒ ํ•˜๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์˜ˆ์ • (์–ธ์  ๊ฐ€...) -> chat ๊ฐ์ฒด์—์„œ ์ง์ „์— ๋ณด๋‚ด์ง„ ์ฑ„ํŒ…์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋ฉด ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ํ•œ๋‹ค !

  • ์™œ์ธ์ง€ App.tsx ์—์„œ wrapper์— ๋ถ€์—ฌํ•œ border ์†์„ฑ์ด ์•ˆ๋จนํžŒ๋‹ค..
    (์ •ํ™•ํ•˜๊ฒŒ๋Š” ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ๋ฐฐ๊ฒฝ์ƒ‰์— ๋ฎ์—ฌ๋ฒ„๋ ค์„œ border radius๋ฅผ ์ ์šฉํ•œ border๊ฐ€ ๋ณด์ด์ง€ ์•Š์•„์š”)
    -> z-index ์†์„ฑ์„ ์ด์šฉํ•ด๋ณด์ž

  • ํ•œ๊ธ€๋งŒ ์ž…๋ ฅํ•˜๋ฉด ๋ ๊ธ€์ž๊ฐ€ ํ•œ๋ฒˆ ๋” ๋ณด๋‚ด์ง€๋Š” ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š”๋ฐ, ์˜์–ด์ผ๋•Œ๋Š” ๋˜ ์•ˆ๋‚˜์˜ค๋Š” ์˜ค๋ฅ˜๊ฐ€.. -> ์ˆ˜์ • ์™„๋ฃŒ !! (feat. @yjoonjang)

  • input์— ์žˆ๋Š” ๊ฐ’์˜ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ์ „์†ก๋ฒ„ํŠผ ํ™œ์„ฑํ™”

  • ์‹œ๊ฐ„ ๋” ์ •ํ™•ํ•˜๊ฒŒ ํ‘œ์‹œํ•˜๊ธฐ

    • ๊ฐ™์€๋‚ ์ผ ๊ฒฝ์šฐ โ†’ n์‹œ๊ฐ„ ์ „
    • ๋‹ค๋ฅธ๋‚ ์ผ ๊ฒฝ์šฐ โ†’ 0์›”0์ผ
profile
๊น€์„ ๋‹ฌ ๊ฐœ๋ฐœ๋ธ”๋กœ๊ทธ
post-custom-banner

0๊ฐœ์˜ ๋Œ“๊ธ€