Storybook을 다양한 Addon과 함께 활용해보면서 사용법 정복하기
velog.io
Recoil, React 앱을 위한 실험적인 상태 관리 라이브러리. React만으로는 어려운 여러 기능을 제공하며, 동시에 React의 최신 기능과 호환 됨
2023-12-06 블로그 읽기 - Storybook
프로젝트 리팩토링 중 스토리북을 알게 되었고, 컴포넌트의 문서화 및 테스팅의 용이성, 그리고 회사들의 모집 공고에서 스토리북도 하나의 기술 스택으로 요구되는 것을 보아 사용해보기로 하였다.
스토리북 공식 문서에 정리되어있는 튜토리얼을 따라해보고 간단하게 스토리를 만들어보는 것 까지는 가능했다.
이 과정에서 든 의문점은 다음과 같다.
Template과 args는 정확히 무엇인지?
스토리북에서 controls로 임의로 데이터를 조작하는 것 외에 데이터를 연결하는 법은?
블로그를 통해 해결한 점
Template
공식문서를 보면 " Template.bind({})는 함수의 복사본을 만드는 표준 JavaScript의 한 기법 " 이라고 소개되어 있다. 하나의 컴포넌트로 다른 args값을 가지는 복수의 스토리를 만들기 위해서 사용되는 기법이다. (복수의 스토리를 만들게 아니라면 template를 안써도 되는 것 같다.)
args
스토리의 props이자 controls에서 임의로 데이터 조작 가능하다.
데이터 연결
진행중인 프로젝트에선 recoil을 사용중이며 스토리에서 recoil을 사용하려면 컴포넌트에 <RecoilRoot>을 감싸줘야만 한다. 처음엔 이를 해결하기 위해서 Template안에 <RecoilRoot>을 감싸주었지만, 전역으로 사용하기 위해 preview.js의 decorators에 넣어주는 방식이 있다는 것을 알았다.
추가적으로 addon과 parameter, decorators에 대해서도 알게되었고, 리팩토링 진행 중 사용의 필요성이나 의문점을 느끼게 된다면 따로 정리해보는 시간을 가져보아야 할 것 같다.
참고 문헌
Storybook addon 정리 블로그 - https://velog.io/@velopert/start-storybook#2-2-actions-%EC%95%A0%EB%93%9C%EC%98%A8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
Storybook 활용 정리 블로그 - https://velog.io/@juno7803/Storybook-Storybook-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0#-%EB%8B%A4%EC%8B%9C-story%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
Storybook에 외부 라이브러리 적용 정리 블로그 -
https://velog.io/@baby_dev/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%99%80-%ED%95%A8%EA%BB%98-%ED%95%98%EB%8A%94-Storybook-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0?ref=codenary#storybook-with-recoil
Recoil Atom Effect 적용 기: 팀 내 설득 및 구현 조언
안녕하세요!!
현재 진행 중인 프로젝트에서 회원가입 시 필요한 정보를 입력하는 중 새로고침 시 session 및 localstorage를 이용하여 상태유지를 하려고하는데 recoil과 관련하여 atom effect를 사용하면 우아하게 처리할 수 있음을 알았습니다.
따라서, 각 atom에 effect를 하나하나 추가하는 방법을 생각했는데 동일한 logic을 행하는 코드의 양이 방대해지고 localstorage에 각 atom에 해당하는 key-value로 저장되므로 가독성 측면에서 좋지 않다고 판단하여 아래와 같은 과정을 생각했습니다.
각 atom을 하나로 묶어 객체로 관리하자.
객체로 관리하게 되면 불필요한 re-rendering이 촉발되므로 객체로 선언한 atom의 각 property에 접근 및 수정을 위한 selector를 정의하자.
객체에 내의 property에 1대1로 selector를 정의하면 객체로 묶기 전 atom의 갯수만큼 selector를 선언해주어야 하므로 selectorFamily를 사용하자.
이것저것 찾아보면서 1 → 2 → 3 단계로 생각을 정리했습니다.
아래는 현재 사용되는 atom입니다.
<ATOM>
// signup.store.ts 👇 회원가입에 대한 user state들
SignUpProfileTypeAtom
SignUpProfileRentalTypeAtom
SignUpProfileRegionsAtom
SignUpProfileDepositPriceAtom
SignUpProfileTermAtom
SignUpProfileMonthlyPriceAtom
SignUpProfileSmokingAtom
SignUpProfilePetAtom
SignUpProfileAppealsAtom
SignUpProfileGenderAtom
SignUpProfileMatesNumberAtom
SignUpProfileMateAppealsAtom
// 👇 위의 atom들을 한 번에 access 및 update
SignUpProfileSelector
하지만, 다른 팀원이 저렇게 atom을 구성한 상황 제 생각을 그대로 적용하고자하면 팀원의 코드를 마음대로 바꾸는 거 같아서 조심스럽습니다.
협업함에 있어 설득도 하나의 중요한 스킬임을 갈수록 깨닫게 됩니다.(다들 어떻게 설득하시나요?)
팀원이 기존의 코드는 안 바꿨으면 좋겠다 하면 각 atom에 effect를 추가하는 것이 맞겠죠????
현재 현업에 계신 분들은 이러한 상황에서 어떻게 설득하며 어떻게 하는 것이 좋을까 자문을 구하고 싶어서 글 올려봅니다!!🥲🥲
짧지 않은 글이지만 읽어주셔서 감사하고 많은 의견 주시면 감사하겠습니다!!😄😄😄
import { SignUpType } from '@/types/signUp.type';
// ? type 집 유형 0: 원룸/오피스텔, 1: 빌라/연립, 2: 아파트, 3: 단독주택 @number
export const SignUpProfileTypeAtom = atom<SignUpType['type']>({
key: 'signUpProfileTypeAtom',
default: undefined,
});
// ? rental_type 집 대여 유형 0: 월세, 1: 전세, 2: 반 전세 @number
export const SignUpProfileRentalTypeAtom = atom<SignUpType['rental_type']>({
key: 'signUpProfileRentalTypeAtom',
default: undefined,
});
// ? regions 유저가 찾는 지역 >도시 (region) + 구(district) 형태의 배열 @string[]
export const SignUpProfileRegionsAtom = atom<SignUpType['regions']>({
key: 'signUpProfileRegionsAtom',
default: [],
});
// ? deposit_price 보증금 (전세 혹은 월세) 범위 [최소 금액, 최대 금액] (0만원~10000만원) @[number, number]
export const SignUpProfileDepositPriceAtom = atom<SignUpType['deposit_price']>({
key: 'signUpProfileDepositPriceAtom',
default: [0, 10000],
});
// ? term 유저가 살 기간 [최소기간, 최대 기간] (0 ~ 24)범위 @[number, number]
export const SignUpProfileTermAtom = atom<SignUpType['term']>({
key: 'signUpProfileTermAtom',
default: [0, 24],
});
// ? monthly_rental_price 월세 [최소 금액, 최대 금액] (0만원, 500만원) @[number, number]
export const SignUpProfileMonthlyPriceAtom = atom<SignUpType['monthly_price']>({
key: 'signUpProfileMonthlyPriceAtom',
default: [0, 500],
});
// ? smoking 흡연 여부 @boolean
export const SignUpProfileSmokingAtom = atom<SignUpType['smoking']>({
key: 'signUpProfileSmokingAtom',
default: undefined,
});
// ? pet 펫 여부 0: 상관없음, 1: 좋음, 2: 싫음 @number
export const SignUpProfilePetAtom = atom<SignUpType['pet']>({
key: 'signUpProfilePetAtom',
default: undefined,
});
// ? appeals 유저의 어필할 매력(배열형태) @string[]
export const SignUpProfileAppealsAtom = atom<SignUpType['appeals']>({
key: 'signUpProfileAppealsAtom',
default: [],
});
// ? gender 상대방의 성별 0: 상관없음, 1: 남성, 2: 여성 @number
export const SignUpProfileGenderAtom = atom<SignUpType['gender']>({
key: 'signUpProfileGenderAtom',
default: undefined,
});
// ? mates_number 인원수 0: 상관없음, 1: 1명, 2: 2명, 3: 3명이상 @number
export const SignUpProfileMatesNumberAtom = atom<SignUpType['mates_number']>({
key: 'signUpProfileMateNumberAtom',
default: undefined,
});
// ? mate_appeals 유저가 원하는 상대방의 매력 (배열형태) @string[]
export const SignUpProfileMateAppealsAtom = atom<SignUpType['mate_appeals']>({
key: 'signUpProfileMateAppealsAtom',
default: [],
});
암호화폐 블록체인 사이드 프로젝트 ‘프론트 개발자'를 모십니다
저희팀은 사이드 프로젝트로 시작하여 스타트업 창업까지 고려하고 있어요! 재미있게 처음부터 끝까지 함께할 개발자분을 구합니다
관심있으신 분들은 🔗오픈카톡을 통해 언제든 문의주세요
https://open.kakao.com/o/spmxPEqg
[ 베이크 팀 구성원 ]
- 기획자 3명
- UX/UI디자이너 1명
- 풀스택 개발자 1명, 프론트 개발자 2명
*(react native 개발자 구인중!)
✨ 서비스 소개
베이크는 국내 유일 한국어 암호화폐 스테이킹 서비스 입니다
🙂 24년 5월 7일 MVP모델 출시를 완료했고, 어려운 암호화폐 서비스를 국내 유저들에게 쉽고 편리하게 제공하는 것이 저희의 목표예요🚩
😻개발자를 구해요😻
👀 이런분을 찾아요
- 암호화폐 생태계에 관심이 있고, 서비스의 초기부터 만들어나가고 싶은 개발자분!
- React, React native, typescript
- recoil, styled-components, next.js 경험자 (필수 X)
- AWS, nestjs, 백엔드 경험자 (필수 X)
- Solidity, web3.js, 블록체인 연동 경험자 (필수 X)
☑️ 상세 기술 스택
프론트 : React, React Native, Recoil, react-query, typescript, next, styled-components
백엔드 : nest.js typescript typeorm, postgres
인프라 : lambda, serverless, api gateway
👇 여기서 서비스를 확인하세요!
베이크 모바일 서비스 (모바일로 링크를 열어주세요!)
app.bake-now.com
베이크 웹사이트 가장 쉬운 암호화폐 예치, Bake
https://www.bake-now.com/
더 자세한 내용은 아래 링크를 통해 확인해주세요!
https://www.notion.so/bakenow/cb30b783b1594bc59c35db45a92cf0dd?pvs=4
관심있으시다면, 어떤 질문이든 아래 오픈카톡을 통해 연락주세요!
https://open.kakao.com/o/spmxPEqg
사이드 프로젝트 암호화폐 서비스 '베이크'에서 프론트 개발자분을 모십니다
안녕하세요 :) 사이드 프로젝트팀 베이크입니다 🙇♂️
국내 유일 한국어 암호화폐 스테이킹 서비스 팀 베이크에서 ‘개발자’ 분을 구인하고 있습니다!!!
관심있으신 분들은 🔗오픈카톡을 통해 언제든 문의주세요!
오픈카톡 : https://open.kakao.com/o/spmxPEqg
✨ 팀 소개
✨베이크 멤버 소개✨ https://www.notion.so/bakenow/41ec1c3e1b334cbab2817746271a9bcb
평균연령 20대 중후반의 젊고 열정있는 팀이에요!
기획 및 마케팅 3명 (카카오 출신!)
UX디자이너 1명
풀스택 개발자 1명 (토스 출신!)
프론트 개발자 2명 (하이퍼커넥트 출신!)
*react 개발자 구인중!
✨ 서비스 소개
베이크는 국내 유일 한국어 암호화폐 스테이킹 서비스 입니다 🙂
24년 5월 7일 MVP모델 출시를 완료했고, 어려운 암호화폐 서비스를 국내 유저들에게 쉽고 편리하게 제공하는 것이 저희의 목표예요🚩
자세한 서비스는 아래 링크를 통해 확인해주세요!
💌 베이크 인스타그램 : https://www.instagram.com/crypto_bakenow/
📥 베이크 모바일 서비스 (모바일로 링크를 열어주세요!) : https://app.bake-now.com/
📥 베이크 웹사이트 : https://www.bake-now.com/
😻개발자를 구해요😻
👀 이런분을 찾아요
암호화폐 생태계에 관심이 있고, 서비스의 초기부터 만들어나가고 싶은 개발자분!
React, React native, typescript
recoil, styled-components, next.js 경험자 (필수 X)
AWS, nestjs, 백엔드 경험자 (필수 X)
Solidity, web3.js, 블록체인 연동 경험자 (필수 X)
☑️ 상세 기술 스택
프론트 : React, React Native, Recoil, react-query, typescript, next, styled-components
백엔드 : nest.js typescript typeorm, postgres
인프라 : lambda, serverless, api gateway
이렇게 참여해요
매주 일요일 오후 10시에 온라인 회의를 진행해요. 야근이 있거나 참석이 어려운날은 말씀해주시면 회의록으로 확인할 수 있어요!
매월 마지막주 토요일 13~19시까지 오프라인 모임을 진행해요
함께하는 사이드 프로젝트 팀원들이 있는 만큼, **참여를 위해 지각비와 불참비가 있어요!
해당 시간에 재미있게 서비스를 만들어갈 개발자분이 필요해요.** 향후 발전할 베이크에서 많은걸 알아가실 수 있어요 😊
관심있으시다면, 어떤 질문이든 아래 오픈카톡을 통해 연락주세요!
프론트엔드 공부방향에 확신이 안섭니다 어떡하죠
부트캠프 수료 후 공부하고 있는데 내가 하는 게 맞는지, 뭘 해야하는지에 확신이 안서요
부끄럽지만 캠프 수료 후 장기간 쉬었습니다(그냥 놀았다는 표현이 맞겠죠)
조금 늦춰졌지만 더 후회안하려고 마음 다시 다잡고 공부를 해보는데 뭐부터 시작해야할지 모르겠어요
일단 제가 진행하고 있는 플랜은 이렇습니다.
개인 프로젝트 하면서 막히는 부분, 에러 핸들링 관련 구글링을 통해 찾아보는 식의 공부 및 경험담 블로그 기록
하루 10p 씩 자바스크립트 딥다이브 정독
프로그래머스 코테 하루 1문제씩 2시간 정도 소모해서 풀어보고 오답노트 정리
기술면접에 대비하여 틈틈히 관련 예상질문, 프론트 기반 cs 지식 공부 및 노션 정리
지금 하고 있는 건 이정도고요. 여기서 이제 추가로 뭔가 더 구현해보고 싶은 기능들이 있어서 (라이브 스트리밍, 채팅) 이 부분은 관련 강의 찾아서 듣고 프로젝트에 접목해보려 합니다
이번 프로젝트 끝내고 바로 다음 하나 더 시작할 예정인데 여기에는 redux로 상태관리를 해볼려고 redux 관련 강의도 들을려고 합니다 (캠프 때부터 recoil만 썼는데 아직 업계에서 redux가 강세라길래 뭔가 배워야하는 의무감이 들어서요)
이런 식으로 부수적인 기능이나 라이브러리는 그때그때 구글링이든 강의든 찾아보면서 배우고 기록하고 하는 식으로 공부하는 게 맞을까요? 뭔가 계획하고 하는 건 많은데 머리에 쌓이는 건 없는 느낌이라 괜히 불안하기만 하네요
한동안 공백기를 보내서 git 잔디도 텅텅 비어있고, 스스로 위기를 조성한 거나 다름없으니 따끔한 조언이라도 넙죽 받겠습니다. 제게 쓰디쓴 충고좀 주십시오 ㅜ
안녕하세요!! 현재 진행 중인 프로젝트에서 회원가입 시 필요한 정보를 입력하는 중 새로고침 시 session 및 localstorage를 이용하여 상태유지를 하려고하는데 recoil과 관련하여 atom effect를 사용하면 우아하게 처리할 수 있음을 알았습니다. 따라서, 각 atom에 effect를 하나하나 추가하는 방법을 생각했는데 동일한 logic을 행하는 코드의 양이 방대해지고 localstorage에 각 atom에 해당하는 key-value로 저장되므로 가독성 측면에서 좋지 않다고 판단하여 아래와 같은 과정을 생각했습니다. 각 atom을 하나로 묶어 객체로 관리하자. 객체로 관리하게 되면 불필요한 re-rendering이 촉발되므로 객체로 선언한 atom의 각 property에 접근 및 수정을 위한 selector를 정의하자. 객체에 내의 property에 1대1로 selector를 정의하면 객체로 묶기 전 atom의 갯수만큼 selector를 선언해주어야 하므로 selectorFamily를 사용하자. 이것저것 찾아보면서 1 → 2 → 3 단계로 생각을 정리했습니다. 아래는 현재 사용되는 atom입니다. <ATOM> // signup.store.ts 👇 회원가입에 대한 user state들 - SignUpProfileTypeAtom - SignUpProfileRentalTypeAtom - SignUpProfileRegionsAtom - SignUpProfileDepositPriceAtom - SignUpProfileTermAtom - SignUpProfileMonthlyPriceAtom - SignUpProfileSmokingAtom - SignUpProfilePetAtom - SignUpProfileAppealsAtom - SignUpProfileGenderAtom - SignUpProfileMatesNumberAtom - SignUpProfileMateAppealsAtom // 👇 위의 atom들을 한 번에 access 및 update - SignUpProfileSelector 하지만, 다른 팀원이 저렇게 atom을 구성한 상황 제 생각을 그대로 적용하고자하면 팀원의 코드를 마음대로 바꾸는 거 같아서 조심스럽습니다. 협업함에 있어 설득도 하나의 중요한 스킬임을 갈수록 깨닫게 됩니다.(다들 어떻게 설득하시나요?) 팀원이 기존의 코드는 안 바꿨으면 좋겠다 하면 각 atom에 effect를 추가하는 것이 맞겠죠???? 현재 현업에 계신 분들은 이러한 상황에서 어떻게 설득하며 어떻게 하는 것이 좋을까 자문을 구하고 싶어서 글 올려봅니다!!🥲🥲 짧지 않은 글이지만 읽어주셔서 감사하고 많은 의견 주시면 감사하겠습니다!!😄😄😄 ```typescript import { SignUpType } from '@/types/signUp.type'; // ? type 집 유형 0: 원룸/오피스텔, 1: 빌라/연립, 2: 아파트, 3: 단독주택 @number export const SignUpProfileTypeAtom = atom<SignUpType['type']>({ key: 'signUpProfileTypeAtom', default: undefined, }); // ? rental_type 집 대여 유형 0: 월세, 1: 전세, 2: 반 전세 @number export const SignUpProfileRentalTypeAtom = atom<SignUpType['rental_type']>({ key: 'signUpProfileRentalTypeAtom', default: undefined, }); // ? regions 유저가 찾는 지역 >도시 (region) + 구(district) 형태의 배열 @string[] export const SignUpProfileRegionsAtom = atom<SignUpType['regions']>({ key: 'signUpProfileRegionsAtom', default: [], }); // ? deposit_price 보증금 (전세 혹은 월세) 범위 [최소 금액, 최대 금액] (0만원~10000만원) @[number, number] export const SignUpProfileDepositPriceAtom = atom<SignUpType['deposit_price']>({ key: 'signUpProfileDepositPriceAtom', default: [0, 10000], }); // ? term 유저가 살 기간 [최소기간, 최대 기간] (0 ~ 24)범위 @[number, number] export const SignUpProfileTermAtom = atom<SignUpType['term']>({ key: 'signUpProfileTermAtom', default: [0, 24], }); // ? monthly_rental_price 월세 [최소 금액, 최대 금액] (0만원, 500만원) @[number, number] export const SignUpProfileMonthlyPriceAtom = atom<SignUpType['monthly_price']>({ key: 'signUpProfileMonthlyPriceAtom', default: [0, 500], }); // ? smoking 흡연 여부 @boolean export const SignUpProfileSmokingAtom = atom<SignUpType['smoking']>({ key: 'signUpProfileSmokingAtom', default: undefined, }); // ? pet 펫 여부 0: 상관없음, 1: 좋음, 2: 싫음 @number export const SignUpProfilePetAtom = atom<SignUpType['pet']>({ key: 'signUpProfilePetAtom', default: undefined, }); // ? appeals 유저의 어필할 매력(배열형태) @string[] export const SignUpProfileAppealsAtom = atom<SignUpType['appeals']>({ key: 'signUpProfileAppealsAtom', default: [], }); // ? gender 상대방의 성별 0: 상관없음, 1: 남성, 2: 여성 @number export const SignUpProfileGenderAtom = atom<SignUpType['gender']>({ key: 'signUpProfileGenderAtom', default: undefined, }); // ? mates_number 인원수 0: 상관없음, 1: 1명, 2: 2명, 3: 3명이상 @number export const SignUpProfileMatesNumberAtom = atom<SignUpType['mates_number']>({ key: 'signUpProfileMateNumberAtom', default: undefined, }); // ? mate_appeals 유저가 원하는 상대방의 매력 (배열형태) @string[] export const SignUpProfileMateAppealsAtom = atom<SignUpType['mate_appeals']>({ key: 'signUpProfileMateAppealsAtom', default: [], }); ```
안녕하세요 답변 드립니다. 😏 기존 사용하는 방법이나 새로운 방법 둘 다 상관은 없을 것 같지만 되도록 한 프로젝트에서는 같은 방식으로 진행 하는 게 좋을 것 같습니다. 바꾸실 꺼면 전체를 다 바꾸시는 게 좋을 것 같습니다. 재 사용을 위해 만들고 재 사용되지 않는 로직은 효율적이라도 크게 의미가 없을 것 같습니다. 그리고 제가 리코일은 사용해 본 적이 없어 잘 모르겠지만 userInfo 하나에 다 정리해서 넣을만한 내용인 것 같은데 저렇게 분리되어야지 더 좋은 구조일까요? 리액트 기반에 리코일 이라면 객체 내 데이터가 화면을 구성하는 요소랑 관계가 없을 때 리랜더링이 일어나지 않았던 것 같은데 🤔 또한 정리된 내용들이 굳이 다 상태 관리로 분리해야 될지도 약간 의문이 듭니다. 어차피 업데이트된 내용이라면 백엔드에 post를 통해 유저의 상태를 업로드 하게 되고 이후 해당 info가 다시 필요하게 된다면 백엔드에 get을 호출 하는 게 더 좋습니다. (너무 빈번하지 않다면요) 상태 관리를 통해 통신을 적절히 최적화하고 횟수를 줄이는 선택은 나쁘지 않지만 서버의 정보와 유저의 정보가 동일하지 않게 되는 시점이 발생할 수 있고 (통신 에러, 로직 에러, 중간에 끄던지 등의 통신 차단) 이러한 정보를 모두 상태 관리로 관리하게 되면 불일치가 일어날 수 있습니다. 요약하자면 한 프로젝트에서는 협의하 통일된 상태 관리 룰을 사용 하는 게 좋으며 현재 사용하는 상태 관리가 과연 필요한 것인지, 가입이나 설정 페이지에서 단순히 useState정도로 끝날 내용이 아닐 지에 대한 고민이 필요할 것 같습니다. + 잘 읽어보니 리랜더링이 요점인지 새로 고침 시의 정보 보존이 목표 인지부터 조금 애매한 듯 하네요 새로 고침 시 정보 유지의 경우 필요하다면 단순히 localStorage와 연결해서 구현 가능합니다. redux나 recoil에서 persist와 같은 라이브러리로 상태 관리 중인 값을 localStorage에 단순 연동도 가능하지만 정보 유지를 위해 기존 useEffect나 변수를 꼭 상태 관리를 거쳐야 하지는 않습니다.
안녕하세요. 현재 Websocket과 stompjs v6.0.0을 활용해 채팅을 구현했습니다. roomId로 여러 채팅방을 만들 수 있게 구현했고, 현재 새로고침을 하지 않는 이상 잘 돌아갑니다. 그러나, 새로고침 할 시에는 바로 연결이 끊겨 이전의 채팅 내역도 보이지 않고, 연결, 구독 내역이 사라집니다 ... 어떻게 reconnect 해야할까요? 단순히 채팅 페이지에서 useEffect로 connect를 다시 하니 이미 연결 구독이 된 상태라고 뜨더라구요 .... ㅠㅠ (고민글을 올렸을 때 채팅방이 생성되고, 연결 구독이 됩니다. 채팅 시작 버튼을 눌렀을 경우에는 본인이 연결 구독이 되어 1대 1로 상대방과 채팅이 시작되는 구조입니다. ) import { CompatClient, Stomp } from "@stomp/stompjs"; import { createContext, useContext, useMemo, useRef } from "react"; import { useSetRecoilState } from "recoil"; import { messageState } from "../../states/chatting"; import audio from "../../assets/audios/chatting.mp3"; const ChatContext = createContext( {} as { connect: (roomId: number) => void; disconnect: () => void; send: (roomId: number, message: string) => void; }, ); export const useChatContext = () => useContext(ChatContext); export function ChatProvider({ children }: any) { const setMessages = useSetRecoilState(messageState); const token = localStorage.getItem("accessToken"); // 채팅 연결 구독 const client = useRef<CompatClient>(); const connect = (roomId: number) => { client.current = Stomp.over(() => { const sock = new WebSocket("wss://m-ssaem.com:8080/stomp/chat"); return sock; }); client.current.connect( { token: token, }, () => { client.current && client.current.subscribe( `/sub/chat/room/${roomId}`, (message) => onMessageReceived(message, roomId), { token: token!, }, ); }, ); return client; }; const onMessageReceived = (message: any, roomId: number) => { const audioElement = new Audio(audio); audioElement.play(); setMessages((prevMessages) => { const updatedMessages = { ...prevMessages, [roomId]: [...(prevMessages[roomId] || []), JSON.parse(message.body)], }; return updatedMessages; }); }; // 채팅 나가기 const disconnect = () => { if (client.current) { client.current.disconnect(() => { window.location.reload(); }); } }; // 채팅 보내기 const send = (roomId: number, message: string) => { if (client.current) { client.current.send( `/pub/chat/message`, { token: token, }, JSON.stringify({ roomId: roomId, message: message, type: "TALK", }), ); } }; const handlers = useMemo(() => ({ connect, disconnect, send }), []); return ( <ChatContext.Provider value={handlers}>{children}</ChatContext.Provider> ); } ----------이 부분은 connect 하는 부분입니다 --------- const { connect } = useChatContext(); const chatRoomId = worryBoard && worryBoard.chatRoomId; const handleStartChatting = () => { navigate(`/chatting`); connect(chatRoomId!!); }; ------------ 채팅 페이지는 따로 있습니다 --------------
하나의 페이지에서 connectHandler를 작동하고 또다른 페이지에서 sendHandler를 작동하려고 하는데 이렇게 해서는 useChat()이 리렌더링 되면서 client 값이 초기화가 되더라구요 값을 유지하고 싶고 recoil에 client를 담는 건 불가능이라고 떠서... connectHandler와 sendHandler를 다른 hooks로 분리하는 방법도 생각해봤는데 그러면 또 client값이 connect한 값이 아니더라구요 무슨 방법이 있을까요? 제발 도와주세요 ㅠㅠ (한 페이지에서 connectHandler, sendHandler, disconnectHandler 실행하면 잘 작동합니다!) import { CompatClient, Stomp } from "@stomp/stompjs"; import { useRef } from "react"; import { useRecoilState } from "recoil"; import { inputMessageState, messageState } from "../../states/chatting"; export function useChat() { const [messages, setMessages] = useRecoilState(messageState); const [inputMessage, setInputMessage] = useRecoilState(inputMessageState); const token = localStorage.getItem("accessToken"); // 채팅 연결 구독 const client = useRef<CompatClient>(); const connectHandler = () => { client.current = Stomp.over(() => { const sock = new WebSocket("wss://m-ssaem.com:8080/stomp/chat"); return sock; }); client.current.connect( { token: token, }, () => { client.current && client.current.subscribe(`/sub/chat/room/1`, onMessageReceived, { token: token!, }); }, ); }; const onMessageReceived = (message: any) => { setMessages((prevMessage) => [...prevMessage, JSON.parse(message.body)]); }; // 채팅 나가기 const disconnectHandler = () => { if (client.current) { client.current.disconnect(() => { window.location.reload(); }); } }; // 채팅 보내기 const sendHandler = () => { if (client.current && inputMessage.trim() !== "") { client.current.send( `/pub/chat/message`, { token: token, }, JSON.stringify({ roomId: 1, message: inputMessage, type: "TALK", }), ); setInputMessage(""); } }; return { connectHandler, disconnectHandler, sendHandler, }; }
지금 구조에선 clientRef는 useChat이 사용되는 곳 마다 초기화 될 수 밖에 없습니다. 방법 중 하나론 Context API로 전역에서 사용하거나 원하는 범위에서 사용하는 방법이 있을 것 같습니다. import React, { createContext, useContext, useState, useEffect } from 'react'; const ChatContext = createContext(); export const ChatProvider = ({ children }) => { const handlers = useMemo(() => ({ connect, disconnect, send }), []) return ( <ChatContext.Provider value={handlers}> {children} </ChatContext.Provider> ); }; export const useChat = () => { return useContext(ChatContext); }; // App.tsx 혹은 어디든.. import { ChatProvider } from './ChatContext'; function App() { return ( /* 리코일 루트보다 하위 */ <ChatProvider> {/* 나머지 컴포넌트들 */} </ChatProvider> ); } export default App; 토큰은 connect 한테 인자로 받는게 좋을거 같습니다. 보다보니 계속 수정하게 되네요. 대신 이렇게 쓰면 리코일 상태는 같이 안쓰는 쪽이 좋을거 같습니다. sendHandler도 인풋메시지를 인자로 받는 편이 좋아보입니다.
회원가입 마지막 단계 페이지인 관심태그 페이지입니다. 이미 nickName, email, phoneNum, pwd, subGroup, workYear는 recoil의 userState에 저장된 상태입니다. 다음 코드를 실행하면 api 호출에는 성공하는데, isSuccess가 false가 되면서 '이메일을 입력해주세요'라는 message가 뜨고 회원가입 실패 alert 창이 뜹니다. 아무래도 recoil value의 전달에 실패한 것 같은데 왜 그럴까요? 참고로 api 주소는 가짜로 적었습니다. function InterestTag() { const [user, setUser] = useRecoilState(userState); const [interestIdx, setInterestIdx] = useRecoilState(userState); const [buttonColor, setButtonColor] = useState("f1f4f7"); const nickName = useRecoilValue(userState).nickName; const email = useRecoilValue(userState).email; const phoneNum = useRecoilValue(userState).phoneNum; const pwd = useRecoilValue(userState).pwd; const subGroup = useRecoilValue(userState).subGroup; const workYear = useRecoilValue(userState).workYear; const handleTagClick = (interestIdx) => { setInterestIdx(interestIdx); setUser({ ...user, interestIdx: interestIdx, isLogin: true, }); setButtonColor("#36f"); }; console.log(interestIdx); const navigate = useNavigate(); const handleSubmit = () => { axios .post("https://abcd.shop/users", { data: { nickName: nickName, email: email, phoneNum: phoneNum, pwd: pwd, subGroup: subGroup, workYear: workYear, interestIdx: interestIdx, }, }) .then((response) => { console.log(response.data); if (response.data.isSuccess === true) { alert("회원가입 성공"); navigate("/"); } else { alert("회원가입 실패"); } }); };
안녕하세요. axios.post로 전송하는 데이터 형태가 의도된 것인가요? { data: { data: { nickname: ... } } } 형태로 날라가는 것 처럼 보여서요. 백엔드가 저렇게 짜여져있다면 처리가 될텐데 혹시 { data: { nickname: ...} } 형태를 받고 있을 수 있어서 질문 드립니다.
안녕하세요. redux를 recoil로 변경하면서 typescript도 같이 사용해보고 있는데 감이 잘 잡히지 않네요 타입스크립트에서 다른타입의 값을 가져와서 비교후 처리해야한다면 어떻게 해야할지 모르겠어서 질문을 드리게 되었습니다. 애초에 이러한 경우는 성립을 하지가 않는걸까요? 컴포넌트에서 deleteTagHandler에서 Tag타입으로 값을 받아와서 setRemoveToNoteTags()로 Tag타입의 매개변수 tag를 전달해주는데요 selector에서는 NotesList의 타입을 지원하고 있어서 그런거 같습니다... 가르침 부탁드리겠습니다 ㅠㅠㅠ 컴포넌트``` const setRemoveToNoteTags = useSetRecoilState(removeTagsSelector); const deleteTagHandler = (tag: Tag): void => { setTagsState({ type: "delete", tagsList: [tag] }); setRemoveToNoteTags(tag); }; ``` selector``` interface NotesList { mainNotes: Note[]; archiveNotes: Note[]; trashNotes: Note[]; editNote: null | Note; } const initialState: NotesList = { mainNotes: [...notes], archiveNotes: [], trashNotes: [], editNote: null, }; export const notesListState = atom({ key: "notesListState", // 고유한 키 default: initialState, // 초기 상태 }); export const removeTagsSelector = selector({ key: "removeTagsSelector", get: ({ get }) => {}, set: ({ get, set }, newValue: Tag) => { const notesList = get(notesListState); const removeTagFromNotes = (notes: Note[]) => { return notes.map((note) => { return { ...note, tags: note.tags.filter(({ tag }) => tag !== newValue.tag), }; }); ...... ```